diff --git a/package-lock.json b/package-lock.json index 5c93e44..cb802cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,17 +13,50 @@ "zustand": "^4.4.7" }, "devDependencies": { + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", "@types/node": "^24.3.1", "@types/react": "^18.2.0", "@vitejs/plugin-react": "^4.2.0", + "@vitest/ui": "^3.2.4", "express": "^4.18.0", + "jsdom": "^26.1.0", "typescript": "^5.0.0", - "vite": "^5.0.0" + "vite": "^5.0.0", + "vitest": "^3.2.4" }, "peerDependencies": { "react": ">=18.0.0" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -258,6 +291,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -306,6 +349,121 @@ "node": ">=6.9.0" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -747,6 +905,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1048,6 +1213,90 @@ "win32" ] }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "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" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.8.0.tgz", + "integrity": "sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1093,6 +1342,23 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1149,6 +1415,143 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.2.4.tgz", + "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.1", + "tinyglobby": "^0.2.14", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "3.2.4" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -1163,6 +1566,51 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -1170,6 +1618,16 @@ "dev": true, "license": "MIT" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -1255,6 +1713,16 @@ "node": ">= 0.8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1307,6 +1775,33 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -1367,6 +1862,27 @@ "node": ">= 0.10" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -1374,6 +1890,20 @@ "devOptional": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.2.tgz", @@ -1392,6 +1922,23 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1402,6 +1949,16 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -1413,6 +1970,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1452,6 +2017,19 @@ "node": ">= 0.8" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1472,6 +2050,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -1541,6 +2126,16 @@ "dev": true, "license": "MIT" }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1551,6 +2146,16 @@ "node": ">= 0.6" } }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -1615,6 +2220,31 @@ "dev": true, "license": "MIT" }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, "node_modules/finalhandler": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", @@ -1651,6 +2281,13 @@ "dev": true, "license": "MIT" }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1784,6 +2421,19 @@ "node": ">= 0.4" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -1801,6 +2451,34 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1814,6 +2492,16 @@ "node": ">=0.10.0" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1831,6 +2519,13 @@ "node": ">= 0.10" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1838,6 +2533,46 @@ "dev": true, "license": "MIT" }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -1864,6 +2599,13 @@ "node": ">=6" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -1874,6 +2616,27 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1950,6 +2713,26 @@ "node": ">= 0.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1993,6 +2776,13 @@ "dev": true, "license": "MIT" }, + "node_modules/nwsapi": { + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", + "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2028,6 +2818,19 @@ "node": ">= 0.8" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2045,6 +2848,23 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2052,6 +2872,19 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -2081,6 +2914,22 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2095,6 +2944,16 @@ "node": ">= 0.10" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -2147,6 +3006,28 @@ "node": ">=0.10.0" } }, + "node_modules/react-dom": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.1" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -2157,6 +3038,20 @@ "node": ">=0.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/rollup": { "version": "4.50.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz", @@ -2198,6 +3093,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2226,6 +3128,27 @@ "dev": true, "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -2387,6 +3310,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2397,6 +3342,13 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -2407,6 +3359,134 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -2417,6 +3497,42 @@ "node": ">=0.6" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -2581,6 +3697,231 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 9cc406f..245bfb0 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,9 @@ "preview": "vite preview", "type-check": "tsc --noEmit", "serve": "npm run build && node server.js", + "test": "vitest", + "test:ui": "vitest --ui", + "test:run": "vitest run", "postversion": "node scripts/update-readme-urls.js" }, "keywords": [ @@ -46,11 +49,16 @@ "react": ">=18.0.0" }, "devDependencies": { + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", "@types/node": "^24.3.1", "@types/react": "^18.2.0", "@vitejs/plugin-react": "^4.2.0", + "@vitest/ui": "^3.2.4", "express": "^4.18.0", + "jsdom": "^26.1.0", "typescript": "^5.0.0", - "vite": "^5.0.0" + "vite": "^5.0.0", + "vitest": "^3.2.4" } } diff --git a/src/auth-store/index.ts b/src/auth-store/index.ts index 44985af..d44bcf2 100644 --- a/src/auth-store/index.ts +++ b/src/auth-store/index.ts @@ -1,6 +1,8 @@ import { create } from "zustand"; import { getOutseta, outsetaLog } from "../outseta"; -import { setNestedProperty, debounce } from "./utils"; +import { setNestedProperty, getNestedProperty, debounce } from "./utils"; + +export { getNestedProperty }; // Type for the original Outseta user object (nested structure) export type OutsetaUser = any; diff --git a/src/framer/overrides/test.tsx b/src/framer/overrides-hello-world.tsx similarity index 100% rename from src/framer/overrides/test.tsx rename to src/framer/overrides-hello-world.tsx diff --git a/src/framer/overrides.tsx b/src/framer/overrides.tsx new file mode 100644 index 0000000..5914237 --- /dev/null +++ b/src/framer/overrides.tsx @@ -0,0 +1,19 @@ +export { + toggleUserProperty, + showForUserProperty, + hideForUserPayloadProperty, + withUserProperty, + withUserImageProperty, +} from "./overrides/user-properties"; + +export { + withPayloadProperty, + showForPayloadProperty, + hideForPayloadProperty, +} from "./overrides/payload-properties"; + +export { + showForAuthStatus, + triggerPopup, + triggerAction, +} from "./overrides/auth"; diff --git a/src/framer/overrides/auth.test.tsx b/src/framer/overrides/auth.test.tsx new file mode 100644 index 0000000..4773a80 --- /dev/null +++ b/src/framer/overrides/auth.test.tsx @@ -0,0 +1,304 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { render } from "@testing-library/react"; +import React from "react"; +import { showForAuthStatus, triggerPopup, triggerAction } from "./auth"; +import { authStore } from "../../auth-store"; + +// Mock the auth store +vi.mock("../../auth-store", () => ({ + authStore: vi.fn(), +})); + +// Mock the utils log function +vi.mock("./utils", () => ({ + log: vi.fn(), +})); + +describe("auth.tsx", () => { + let mockAuthStore: any; + let TestComponent: React.ComponentType; + + beforeEach(() => { + // Create a mock component for testing + TestComponent = React.forwardRef((props, ref) => ( +
+ Test Component +
+ )); + + // Reset mocks + vi.clearAllMocks(); + + // Mock window.location for Framer canvas detection + Object.defineProperty(window, "location", { + value: { + host: "localhost:3000", + href: "http://localhost:3000", + }, + writable: true, + }); + + // Setup mock auth store + mockAuthStore = vi.fn(); + vi.mocked(authStore).mockImplementation(mockAuthStore); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("authenticated", () => { + beforeEach(() => { + mockAuthStore.mockImplementation((selector: any) => { + const state = { + status: "authenticated", + payload: { sub: 1 }, + user: { Uid: 1, FullName: "Test User" }, + }; + return selector(state); + }); + }); + + describe("showForAuthStatus: pending", () => { + it("should NOT render the component", () => { + const WrappedComponent = showForAuthStatus(TestComponent, "pending"); + const { queryByTestId } = render(); + expect(queryByTestId("test-component")).not.toBeInTheDocument(); + }); + }); + + describe("showForAuthStatus: anonymous", () => { + it("should NOT render the component", () => { + const WrappedComponent = showForAuthStatus(TestComponent, "anonymous"); + const { queryByTestId } = render(); + expect(queryByTestId("test-component")).not.toBeInTheDocument(); + }); + + it("should render the component when in Framer canvas", () => { + Object.defineProperty(window, "location", { + value: { + host: "test.framercanvas.com", + href: "https://test.framercanvas.com", + }, + writable: true, + }); + + const WrappedComponent = showForAuthStatus(TestComponent, "anonymous"); + const { getByTestId } = render(); + expect(getByTestId("test-component")).toBeInTheDocument(); + }); + }); + + describe("showForAuthStatus: authenticated", () => { + it("should render the component", () => { + const WrappedComponent = showForAuthStatus( + TestComponent, + "authenticated" + ); + const { getByTestId } = render(); + expect(getByTestId("test-component")).toBeInTheDocument(); + }); + }); + + describe("showForAuthStatus: user-loaded", () => { + it("should render the component when user exists", () => { + const WrappedComponent = showForAuthStatus( + TestComponent, + "user-loaded" + ); + const { getByTestId } = render(); + expect(getByTestId("test-component")).toBeInTheDocument(); + }); + + it("should NOT render the component when no user", () => { + // Override mock for this specific test + mockAuthStore.mockImplementation((selector: any) => { + const state = { + status: "authenticated", + user: null, + payload: { sub: 1 }, + }; + return selector(state); + }); + + const WrappedComponent = showForAuthStatus( + TestComponent, + "user-loaded" + ); + const { queryByTestId } = render(); + expect(queryByTestId("test-component")).not.toBeInTheDocument(); + }); + }); + + describe("triggerPopup: register", () => { + it("should NOT render the component when not in Framer", () => { + const WrappedComponent = triggerPopup(TestComponent, "register"); + const { queryByTestId } = render(); + expect(queryByTestId("test-component")).not.toBeInTheDocument(); + }); + + it("should render the component when in Framer canvas", () => { + Object.defineProperty(window, "location", { + value: { + host: "test.framercanvas.com", + href: "https://test.framercanvas.com", + }, + writable: true, + }); + + const WrappedComponent = triggerPopup(TestComponent, "register"); + const { getByTestId } = render(); + const component = getByTestId("test-component"); + expect(component).toHaveAttribute("data-mode", "popup"); + expect(component).toHaveAttribute("data-o-auth", "1"); + expect(component).toHaveAttribute("data-widget-mode", "register"); + }); + }); + + describe("triggerPopup: profile", () => { + it("should render the component", () => { + const WrappedComponent = triggerPopup(TestComponent, "profile"); + const { getByTestId } = render(); + const component = getByTestId("test-component"); + expect(component).toHaveAttribute("data-mode", "popup"); + expect(component).toHaveAttribute("data-o-profile", "1"); + }); + }); + + describe("triggerAction: logout", () => { + it("should render the component", () => { + const WrappedComponent = triggerAction(TestComponent, "logout"); + const { getByTestId } = render(); + const component = getByTestId("test-component"); + expect(component).toHaveAttribute("data-o-logout-link", "1"); + }); + }); + }); + + describe("anonymous", () => { + beforeEach(() => { + mockAuthStore.mockImplementation((selector: any) => { + const state = { status: "anonymous", payload: null, user: null }; + return selector(state); + }); + }); + + describe("showForAuthStatus: anonymous", () => { + it("should render the component", () => { + const WrappedComponent = showForAuthStatus(TestComponent, "anonymous"); + const { getByTestId } = render(); + expect(getByTestId("test-component")).toBeInTheDocument(); + }); + }); + + describe("showForAuthStatus: authenticated", () => { + it("should NOT render the component", () => { + const WrappedComponent = showForAuthStatus( + TestComponent, + "authenticated" + ); + const { queryByTestId } = render(); + expect(queryByTestId("test-component")).not.toBeInTheDocument(); + }); + }); + + describe("showForAuthStatus: pending", () => { + it("should NOT render the component", () => { + const WrappedComponent = showForAuthStatus(TestComponent, "pending"); + const { queryByTestId } = render(); + expect(queryByTestId("test-component")).not.toBeInTheDocument(); + }); + }); + + describe("triggerPopup: register", () => { + it("should render the component", () => { + const WrappedComponent = triggerPopup(TestComponent, "register"); + const { getByTestId } = render(); + const component = getByTestId("test-component"); + expect(component).toHaveAttribute("data-mode", "popup"); + expect(component).toHaveAttribute("data-o-auth", "1"); + expect(component).toHaveAttribute("data-widget-mode", "register"); + }); + }); + + describe("triggerPopup: profile", () => { + it("should NOT render the component", () => { + const WrappedComponent = triggerPopup(TestComponent, "profile"); + const { queryByTestId } = render(); + expect(queryByTestId("test-component")).not.toBeInTheDocument(); + }); + }); + + describe("triggerAction: logout", () => { + it("should NOT render the component", () => { + const WrappedComponent = triggerAction(TestComponent, "logout"); + const { queryByTestId } = render(); + expect(queryByTestId("test-component")).not.toBeInTheDocument(); + }); + }); + }); + + describe("pending", () => { + beforeEach(() => { + mockAuthStore.mockImplementation((selector: any) => { + const state = { status: "pending", user: null, payload: null }; + return selector(state); + }); + }); + + describe("showForAuthStatus: pending", () => { + it("should render the component", () => { + const WrappedComponent = showForAuthStatus(TestComponent, "pending"); + const { getByTestId } = render(); + expect(getByTestId("test-component")).toBeInTheDocument(); + }); + }); + + describe("showForAuthStatus: anonymous", () => { + it("should render the component when in Framer canvas", () => { + Object.defineProperty(window, "location", { + value: { + host: "test.framercanvas.com", + href: "https://test.framercanvas.com", + }, + writable: true, + }); + + const WrappedComponent = showForAuthStatus(TestComponent, "anonymous"); + const { getByTestId } = render(); + expect(getByTestId("test-component")).toBeInTheDocument(); + }); + }); + + describe("showForAuthStatus: authenticated", () => { + it("should NOT render the component", () => { + const WrappedComponent = showForAuthStatus( + TestComponent, + "authenticated" + ); + const { queryByTestId } = render(); + expect(queryByTestId("test-component")).not.toBeInTheDocument(); + }); + }); + }); + + describe("Framer canvas detection", () => { + it("should handle window.location access errors gracefully", () => { + Object.defineProperty(window, "location", { + get: () => { + throw new Error("Access denied"); + }, + configurable: true, + }); + + mockAuthStore.mockImplementation((selector: any) => { + const state = { status: "anonymous", user: null }; + return selector(state); + }); + + const WrappedComponent = showForAuthStatus(TestComponent, "anonymous"); + const { getByTestId } = render(); + expect(getByTestId("test-component")).toBeInTheDocument(); + }); + }); +}); diff --git a/src/framer/overrides/auth.tsx b/src/framer/overrides/auth.tsx new file mode 100644 index 0000000..7b5a9d0 --- /dev/null +++ b/src/framer/overrides/auth.tsx @@ -0,0 +1,192 @@ +import React, { forwardRef } from "react"; +import { authStore, type AuthStatus } from "../../auth-store"; +import { log } from "./utils"; + +/** + * Detects if the current environment is Framer canvas mode + * @returns true if running in Framer canvas + */ +function isFramerCanvas(): boolean { + try { + return window.location.host.includes("framercanvas.com"); + } catch (error) { + // If any detection method fails, assume we're not in Framer + return false; + } +} + +/** + * Shows a component only when the authentication state matches the specified state + * When in Framer canvas or preview mode, treats "anonymous" status as true + * + * @param Component - The React component to conditionally render + * @param validState - The authentication state that allows the component to show + * @returns A forwarded ref component that conditionally renders based on auth state + */ +export function showForAuthStatus( + Component: React.ComponentType, + validStatus: AuthStatus | "user-loaded" +): React.ComponentType { + return forwardRef((props, ref) => { + const logPrefix = `showForAuthStatus ${validStatus} -|`; + + try { + const status = authStore((state) => state.status); + const user = authStore((state) => state.user); + const isFramerEnv = isFramerCanvas(); + + log(logPrefix, { status, validStatus, isFramerEnv }); + + switch (validStatus) { + case "user-loaded": + if (!user) { + throw new Error("User not loaded"); + } + break; + + case "anonymous": + if (!isFramerEnv && status !== "anonymous") { + throw new Error("Not in anonymous state"); + } + break; + + case "authenticated": + if (status !== "authenticated") { + throw new Error("Not in authenticated state"); + } + break; + + case "pending": + if (status !== "pending") { + throw new Error("Not in pending state"); + } + break; + + default: + throw new Error("Invalid status"); + } + + log(logPrefix, "Status match, showing component"); + return ; + } catch (error) { + if (error instanceof Error) { + log(logPrefix, "Hiding component", error.message); + } else { + log(logPrefix, "Hiding component", error); + } + return null; + } + }); +} + +/** + * Converts a component to trigger Outseta popup embeds + * Handles registration, login, and profile popup embeds with proper visibility rules + * + * @param Component - The React component to wrap + * @param embed - The type of Outseta popup embed: "register", "login", or "profile" + * @returns A forwarded ref component that triggers the specified Outseta popup + */ +export function triggerPopup( + Component: React.ComponentType, + embed: "register" | "login" | "profile" +): React.ComponentType { + return forwardRef((props, ref) => { + const logPrefix = `triggerPopup ${embed} -|`; + + try { + const status = authStore((state) => state.status); + const isFramerEnv = isFramerCanvas(); + + log(logPrefix, { status, isFramerEnv }); + + // Set appropriate data attributes based on embed type + const dataAttributes: Record = { + "data-mode": "popup", + }; + + switch (embed) { + case "register": + if (!isFramerEnv && status !== "anonymous") { + throw new Error("Not anonymous"); + } + dataAttributes["data-o-auth"] = "1"; + dataAttributes["data-widget-mode"] = "register"; + break; + case "login": + if (!isFramerEnv && status !== "anonymous") { + throw new Error("Not anonymous"); + } + dataAttributes["data-o-auth"] = "1"; + dataAttributes["data-widget-mode"] = "login"; + break; + case "profile": + if (status !== "authenticated") { + throw new Error("Not authenticated"); + } + dataAttributes["data-o-profile"] = "1"; + break; + default: + throw new Error("Invalid embed type"); + } + + log(logPrefix, "Setting Outseta data attributes", { dataAttributes }); + return ; + } catch (error) { + if (error instanceof Error) { + log(logPrefix, "Hiding component", error.message); + } else { + log(logPrefix, "Hiding component", error); + } + return null; + } + }); +} + +/** + * Converts a component to trigger Outseta action embeds + * Handles logout actions with proper visibility rules + * + * @param Component - The React component to wrap + * @param action - The type of action: "logout" + * @returns A forwarded ref component that triggers the specified Outseta action + */ +export function triggerAction( + Component: React.ComponentType, + action: "logout" +): React.ComponentType { + return forwardRef((props, ref) => { + const logPrefix = `triggerAction ${action} -|`; + + try { + const status = authStore((state) => state.status); + + log(logPrefix, { status }); + + // Set appropriate data attributes based on action type + const dataAttributes: Record = {}; + + switch (action) { + case "logout": + if (status !== "authenticated") { + throw new Error("Not authenticated"); + } + dataAttributes["data-o-logout-link"] = "1"; + break; + + default: + throw new Error("Invalid action type"); + } + + log(logPrefix, "Setting Outseta data attributes", { dataAttributes }); + return ; + } catch (error) { + if (error instanceof Error) { + log(logPrefix, "Hiding component", error.message); + } else { + log(logPrefix, "Hiding component", error); + } + return null; + } + }); +} diff --git a/src/framer/overrides/index.tsx b/src/framer/overrides/index.tsx deleted file mode 100644 index b3e180a..0000000 --- a/src/framer/overrides/index.tsx +++ /dev/null @@ -1,644 +0,0 @@ -import React, { forwardRef } from "react"; -import { authStore, type AuthStatus } from "../../auth-store"; -import { outsetaLog } from "../../outseta"; -import { getNestedProperty } from "../../auth-store/utils"; - -import { compare } from "./compare"; - -type PropertyOptions = { - name: string; -}; - -type ComparePropertyOptions = { - name: string; - value: any; - compare?: "equal" | "array-includes"; - flags?: "ignore-case"[]; -}; - -const log = outsetaLog("framer.overrides"); - -/** - * Detects if the current environment is Framer canvas mode - * @returns true if running in Framer canvas - */ -function isFramerCanvas(): boolean { - try { - return window.location.host.includes("framercanvas.com"); - } catch (error) { - // If any detection method fails, assume we're not in Framer - return false; - } -} - -/** - * Creates a toggle action for any property - * @param Component - The component to wrap - * @param options - Configuration - * @param options.name - Property name - * @param options.value - Value to toggle (can be "props.propertyName") - */ -export function toggleUserProperty( - Component: React.ComponentType, - { name: propertyName, value: toggleValue }: { name: string; value: string } -): React.ComponentType { - return forwardRef((props, ref) => { - const logPrefix = `toggleUserProperty ${propertyName} -|`; - try { - const user = authStore((state) => state.user); - const updateUserProperty = authStore((state) => state.updateUserProperty); - - if (!user) { - throw new Error("User loaded required"); - } - - // Resolve toggle value from props if needed - let resolvedToggleValue = resolveValue(toggleValue, props); - - const propertyValue = getNestedProperty(user, propertyName) || ""; - const propertyValueAsArray = - typeof propertyValue === "string" - ? propertyValue.split(",").map((item) => item.trim()) - : []; - - const handleClick = async (event: React.MouseEvent) => { - event.preventDefault(); - - log(logPrefix, { propertyName, resolvedToggleValue }); - - const newPropertyValueAsArray = [ - // Filter out the toggle value and empty values - ...propertyValueAsArray.filter( - (item) => item !== resolvedToggleValue && item.trim() !== "" - ), - // Add toggle value at the end if not included in current value - ...(propertyValueAsArray.includes(resolvedToggleValue) - ? [] - : [resolvedToggleValue]), - ].filter((item) => item.trim() !== ""); // Final filter to remove any empty values - - await updateUserProperty( - propertyName, - newPropertyValueAsArray.join(", ") - ); - - // Call original onClick if provided - if (props.onClick) { - props.onClick(event); - } - }; - - const active = propertyValueAsArray.includes(resolvedToggleValue); - - return ( - - ); - } catch (error) { - if (error instanceof Error) { - log(logPrefix, "Hiding component", error.message); - } else { - log(logPrefix, "Hiding component", error); - } - return null; - } - }); -} - -/** - * Shows component based on property comparison - * @param Component - The component to wrap - * @param options - Configuration - * @param options.name - Property name to check - * @param options.value - Value to compare against (can be "props.propertyName") - * @param options.compare - Comparison type: "equal" or "array-includes" - * @param options.flags - Additional flags like ["ignore-case"] - */ -export function showForUserProperty( - Component: React.ComponentType, - { - name: propertyName, - value, - compare: compareType = "equal", - flags = [], - }: ComparePropertyOptions -): React.ComponentType { - return forwardRef((props, ref) => { - const logPrefix = `showForUserProperty ${propertyName} -|`; - - try { - const user = authStore((state) => state.user); - - if (!user) { - throw new Error("User loaded required"); - } - const propertyValue = getNestedProperty(user, propertyName); - const resolvedValue = resolveValue(value, props); - - log(logPrefix, { - propertyValue, - resolvedValue, - compareType, - }); - - if (!compare(propertyValue, resolvedValue, compareType, flags)) { - throw new Error("Match not found - hiding component"); - } - - log(logPrefix, "Match found - showing component"); - return ; - } catch (error) { - if (error instanceof Error) { - log(logPrefix, "Hiding component", error.message); - } else { - log(logPrefix, "Hiding component", error); - } - return null; - } - }); -} - -/** - * Sets component text to a user property value - * @param Component - The component to wrap - * @param options - Configuration - * @param options.name - Property name to display - */ -export function withUserProperty( - Component: React.ComponentType, - { name: propertyName }: PropertyOptions -): React.ComponentType { - return forwardRef((props, ref) => { - const logPrefix = `withUserProperty ${propertyName} -|`; - - try { - const user = authStore((state) => state.user); - - if (!user) { - throw new Error("User loaded required"); - } - - let propertyValue = getNestedProperty(user, propertyName); - - if (typeof propertyValue !== "string") { - throw new Error("Not a string"); - } - - log(logPrefix, { propertyValue }); - return ; - } catch (error) { - if (error instanceof Error) { - log(logPrefix, "Hiding component", error.message); - } else { - log(logPrefix, "Hiding component", error); - } - return null; - } - }); -} - -/** - * Sets component background image to a user property value - * @param Component - The component to wrap - * @param options - Configuration - * @param options.name - Property name containing image URL - */ -export function withUserImageProperty( - Component: React.ComponentType, - { name: propertyName }: PropertyOptions -): React.ComponentType { - return forwardRef((props, ref) => { - const logPrefix = `withUserImageProperty ${propertyName} -|`; - - try { - const user = authStore((state) => state.user); - - if (!user) { - throw new Error("User loaded required"); - } - - const imageSrc = getNestedProperty(user, propertyName) as string; - - log(logPrefix, { imageSrc }); - - if (!imageSrc) { - // If no image from user property, use component's default or hide - if (props.background?.src) { - // Component has image set, use as fallback - log(logPrefix, "No user image, using component fallback"); - return ; - } else { - // Component has no image set, hide - log(logPrefix, "No user image and no component fallback, hiding"); - return null; - } - } - - log(logPrefix, "Setting image from user property"); - return ( - - ); - } catch (error) { - if (error instanceof Error) { - log(logPrefix, "Hiding component", error.message); - } else { - log(logPrefix, "Hiding component", error); - } - return null; - } - }); -} - -/** - * Sets component text to a payload property value - * @param Component - The component to wrap - * @param options - Configuration - * @param options.name - Payload property name to display - */ -export function withPayloadProperty( - Component: React.ComponentType, - { name: propertyName }: PropertyOptions -): React.ComponentType { - return forwardRef((props, ref) => { - const logPrefix = `withPayloadProperty ${propertyName} -|`; - - try { - const payload = authStore((state) => state.payload); - - if (!payload) { - throw new Error("Authentication required"); - } - - const propertyValue = getNestedProperty(payload, propertyName); - - if (typeof propertyValue !== "string") { - throw new Error("Not a string"); - } - - log(logPrefix, { propertyValue }); - return ; - } catch (error) { - if (error instanceof Error) { - log(logPrefix, "Hiding component", error.message); - } else { - log(logPrefix, "Hiding component", error); - } - return null; - } - }); -} - -/** - * Shows component based on payload property comparison - * @param Component - The component to wrap - * @param options - Configuration - * @param options.name - Payload property name to check - * @param options.value - Value to compare against (can be "props.propertyName") - * @param options.compare - Comparison type: "equal" or "array-includes" - * @param options.flags - Additional flags like ["ignore-case"] - */ -export function showForPayloadProperty( - Component: React.ComponentType, - { - name: propertyName, - value, - compare: compareType = "equal", - flags = [], - }: ComparePropertyOptions -): React.ComponentType { - // Create comparison function based on compare type - return forwardRef((props, ref) => { - const logPrefix = `showForPayloadProperty ${propertyName} -|`; - - try { - const payload = authStore((state) => state.payload); - - if (!payload) { - throw new Error("Authentication required"); - } - - const propertyValue = getNestedProperty(payload, propertyName); - const resolvedValue = resolveValue(value, props); - - log(logPrefix, { propertyValue, resolvedValue, compareType }); - - if (!compare(propertyValue, resolvedValue, compareType, flags)) { - throw new Error("Match not found - hiding component"); - } - - log(logPrefix, "Match found - showing component"); - return ; - } catch (error) { - if (error instanceof Error) { - log(logPrefix, "Hiding component", error.message); - } else { - log(logPrefix, "Hiding component", error); - } - return null; - } - }); -} - -/** - * Hides component based on payload property comparison - * @param Component - The component to wrap - * @param options - Configuration - * @param options.name - Payload property name to check - * @param options.value - Value to compare against (can be "props.propertyName") - * @param options.compare - Comparison type: "equal" or "array-includes" - * @param options.flags - Additional flags like ["ignore-case"] - */ -export function hideForPayloadProperty( - Component: React.ComponentType, - { - name: propertyName, - value, - compare: compareType = "equal", - flags = [], - }: ComparePropertyOptions -): React.ComponentType { - return forwardRef((props, ref) => { - const logPrefix = `hideForPayloadProperty ${propertyName} -|`; - - try { - const payload = authStore((state) => state.payload); - - if (!payload) { - throw new Error("Authentication required"); - } - - const propertyValue = getNestedProperty(payload, propertyName); - const resolvedValue = resolveValue(value, props); - - log(logPrefix, { propertyValue, resolvedValue, compareType }); - - if (compare(propertyValue, resolvedValue, compareType, flags)) { - throw new Error("Match found - hiding component"); - } - - log(logPrefix, "Match not found - showing component"); - return ; - } catch (error) { - if (error instanceof Error) { - log(logPrefix, "Hiding component", error.message); - } else { - log(logPrefix, "Hiding component", error); - } - return null; - } - }); -} - -/** - * Hides component based on user property comparison - * @param Component - The component to wrap - * @param options - Configuration - * @param options.name - Property name to check - * @param options.value - Value to compare against (can be "props.propertyName") - * @param options.compare - Comparison type: "equal" or "array-includes" - * @param options.flags - Additional flags like ["ignore-case"] - */ -export function hideForUserPayloadProperty( - Component: React.ComponentType, - { - name: propertyName, - value, - compare: compareType = "equal", - flags = [], - }: ComparePropertyOptions -): React.ComponentType { - return forwardRef((props, ref) => { - const logPrefix = `hideForUserPayloadProperty ${propertyName} -|`; - - try { - const user = authStore((state) => state.user); - - if (!user) { - throw new Error("User loaded required"); - } - const propertyValue = getNestedProperty(user, propertyName); - const resolvedValue = resolveValue(value, props); - - log(logPrefix, { - propertyValue, - resolvedValue, - compareType, - }); - - if (compare(propertyValue, resolvedValue, compareType, flags)) { - throw new Error("Match found - hiding component"); - } - - log(logPrefix, "Match not found - showing component"); - return ; - } catch (error) { - if (error instanceof Error) { - log(logPrefix, "Hiding component", error.message); - } else { - log(logPrefix, "Hiding component", error); - } - return null; - } - }); -} - -/** - * Shows a component only when the authentication state matches the specified state - * When in Framer canvas or preview mode, treats "anonymous" status as true - * - * @param Component - The React component to conditionally render - * @param validState - The authentication state that allows the component to show - * @returns A forwarded ref component that conditionally renders based on auth state - */ -export function showForAuthStatus( - Component: React.ComponentType, - validStatus: AuthStatus | "user-loaded" -): React.ComponentType { - return forwardRef((props, ref) => { - const logPrefix = `showForAuthStatus ${validStatus} -|`; - - try { - const status = authStore((state) => state.status); - const user = authStore((state) => state.user); - const isFramerEnv = isFramerCanvas(); - - log(logPrefix, { status, validStatus, isFramerEnv }); - - switch (validStatus) { - case "user-loaded": - if (!user) { - throw new Error("User not loaded"); - } - break; - - case "anonymous": - if (!isFramerEnv && status !== "anonymous") { - throw new Error("Not in anonymous state"); - } - break; - - case "authenticated": - if (status !== "authenticated") { - throw new Error("Not in authenticated state"); - } - break; - - case "pending": - if (status !== "pending") { - throw new Error("Not in pending state"); - } - break; - - default: - throw new Error("Invalid status"); - } - - log(logPrefix, "Status match, showing component"); - return ; - } catch (error) { - if (error instanceof Error) { - log(logPrefix, "Hiding component", error.message); - } else { - log(logPrefix, "Hiding component", error); - } - return null; - } - }); -} - -/** - * Converts a component to trigger Outseta popup embeds - * Handles registration, login, and profile popup embeds with proper visibility rules - * - * @param Component - The React component to wrap - * @param embed - The type of Outseta popup embed: "register", "login", or "profile" - * @returns A forwarded ref component that triggers the specified Outseta popup - */ -export function triggerPopup( - Component: React.ComponentType, - embed: "register" | "login" | "profile" -): React.ComponentType { - return forwardRef((props, ref) => { - const logPrefix = `triggerPopup ${embed} -|`; - - try { - const status = authStore((state) => state.status); - const isFramerEnv = isFramerCanvas(); - - log(logPrefix, { status, isFramerEnv }); - - // Set appropriate data attributes based on embed type - const dataAttributes: Record = { - "data-mode": "popup", - }; - - switch (embed) { - case "register": - if (!isFramerEnv && status !== "anonymous") { - throw new Error("Not anonymous"); - } - dataAttributes["data-o-auth"] = "1"; - dataAttributes["data-widget-mode"] = "register"; - break; - case "login": - if (!isFramerEnv && status !== "anonymous") { - throw new Error("Not anonymous"); - } - dataAttributes["data-o-auth"] = "1"; - dataAttributes["data-widget-mode"] = "login"; - break; - case "profile": - if (status !== "authenticated") { - throw new Error("Not authenticated"); - } - dataAttributes["data-o-profile"] = "1"; - break; - default: - throw new Error("Invalid embed type"); - } - - log(logPrefix, "Setting Outseta data attributes", { dataAttributes }); - return ; - } catch (error) { - if (error instanceof Error) { - log(logPrefix, "Hiding component", error.message); - } else { - log(logPrefix, "Hiding component", error); - } - return null; - } - }); -} - -/** - * Converts a component to trigger Outseta action embeds - * Handles logout actions with proper visibility rules - * - * @param Component - The React component to wrap - * @param action - The type of action: "logout" - * @returns A forwarded ref component that triggers the specified Outseta action - */ -export function triggerAction( - Component: React.ComponentType, - action: "logout" -): React.ComponentType { - return forwardRef((props, ref) => { - const logPrefix = `triggerAction ${action} -|`; - - try { - const status = authStore((state) => state.status); - - log(logPrefix, { status }); - - // Set appropriate data attributes based on action type - const dataAttributes: Record = {}; - - switch (action) { - case "logout": - if (status !== "authenticated") { - throw new Error("Not authenticated"); - } - dataAttributes["data-o-logout-link"] = "1"; - break; - - default: - throw new Error("Invalid action type"); - } - - log(logPrefix, "Setting Outseta data attributes", { dataAttributes }); - return ; - } catch (error) { - if (error instanceof Error) { - log(logPrefix, "Hiding component", error.message); - } else { - log(logPrefix, "Hiding component", error); - } - return null; - } - }); -} - -/** - * Resolves a value from props if it starts with "props." - * @param value - The value to resolve - * @param props - The component props - * @returns The resolved value - */ -function resolveValue(value: any, props: any) { - if (typeof value === "string" && value.startsWith("props.")) { - return props[value.replace("props.", "")]; - } - return value; -} diff --git a/src/framer/overrides/payload-properties.tsx b/src/framer/overrides/payload-properties.tsx new file mode 100644 index 0000000..e5a835c --- /dev/null +++ b/src/framer/overrides/payload-properties.tsx @@ -0,0 +1,149 @@ +import React, { forwardRef } from "react"; +import { authStore, getNestedProperty } from "../../auth-store"; +import { + log, + resolveValue, + compare, + type PropertyOptions, + type ComparePropertyOptions, +} from "./utils"; + +/** + * Sets component text to a payload property value + * @param Component - The component to wrap + * @param options - Configuration + * @param options.name - Payload property name to display + */ +export function withPayloadProperty( + Component: React.ComponentType, + { name: propertyName }: PropertyOptions +): React.ComponentType { + return forwardRef((props, ref) => { + const logPrefix = `withPayloadProperty ${propertyName} -|`; + + try { + const payload = authStore((state) => state.payload); + + if (!payload) { + throw new Error("Authentication required"); + } + + const propertyValue = getNestedProperty(payload, propertyName); + + if (typeof propertyValue !== "string") { + throw new Error("Not a string"); + } + + log(logPrefix, { propertyValue }); + return ; + } catch (error) { + if (error instanceof Error) { + log(logPrefix, "Hiding component", error.message); + } else { + log(logPrefix, "Hiding component", error); + } + return null; + } + }); +} + +/** + * Shows component based on payload property comparison + * @param Component - The component to wrap + * @param options - Configuration + * @param options.name - Payload property name to check + * @param options.value - Value to compare against (can be "props.propertyName") + * @param options.compare - Comparison type: "equal" or "array-includes" + * @param options.flags - Additional flags like ["ignore-case"] + */ +export function showForPayloadProperty( + Component: React.ComponentType, + { + name: propertyName, + value, + compare: compareType = "equal", + flags = [], + }: ComparePropertyOptions +): React.ComponentType { + // Create comparison function based on compare type + return forwardRef((props, ref) => { + const logPrefix = `showForPayloadProperty ${propertyName} -|`; + + try { + const payload = authStore((state) => state.payload); + + if (!payload) { + throw new Error("Authentication required"); + } + + const propertyValue = getNestedProperty(payload, propertyName); + const resolvedValue = resolveValue(value, props); + + log(logPrefix, { propertyValue, resolvedValue, compareType }); + + if (!compare(propertyValue, resolvedValue, compareType, flags)) { + throw new Error("Match not found - hiding component"); + } + + log(logPrefix, "Match found - showing component"); + return ; + } catch (error) { + if (error instanceof Error) { + log(logPrefix, "Hiding component", error.message); + } else { + log(logPrefix, "Hiding component", error); + } + return null; + } + }); +} + +/** + * Hides component based on payload property comparison + * @param Component - The component to wrap + * @param options - Configuration + * @param options.name - Payload property name to check + * @param options.value - Value to compare against (can be "props.propertyName") + * @param options.compare - Comparison type: "equal" or "array-includes" + * @param options.flags - Additional flags like ["ignore-case"] + */ +export function hideForPayloadProperty( + Component: React.ComponentType, + { + name: propertyName, + value, + compare: compareType = "equal", + flags = [], + }: ComparePropertyOptions +): React.ComponentType { + return forwardRef((props, ref) => { + const logPrefix = `hideForPayloadProperty ${propertyName} -|`; + + try { + const payload = authStore((state) => state.payload); + + if (!payload) { + throw new Error("Authentication required"); + } + + const propertyValue = getNestedProperty(payload, propertyName); + const resolvedValue = resolveValue(value, props); + + log(logPrefix, { propertyValue, resolvedValue, compareType }); + + if (compare(propertyValue, resolvedValue, compareType, flags)) { + throw new Error("Match found - hiding component"); + } + + log(logPrefix, "Match not found - showing component"); + return ; + } catch (error) { + if (error instanceof Error) { + log(logPrefix, "Hiding component", error.message); + } else { + log(logPrefix, "Hiding component", error); + } + return null; + } + }); +} diff --git a/src/framer/overrides/user-properties.tsx b/src/framer/overrides/user-properties.tsx new file mode 100644 index 0000000..f4ec1b5 --- /dev/null +++ b/src/framer/overrides/user-properties.tsx @@ -0,0 +1,292 @@ +import React, { forwardRef } from "react"; +import { authStore, getNestedProperty } from "../../auth-store"; +import { compare } from "./utils"; +import { + log, + resolveValue, + type PropertyOptions, + type ComparePropertyOptions, +} from "./utils"; + +/** + * Creates a toggle action for any property + * @param Component - The component to wrap + * @param options - Configuration + * @param options.name - Property name + * @param options.value - Value to toggle (can be "props.propertyName") + */ +export function toggleUserProperty( + Component: React.ComponentType, + { name: propertyName, value: toggleValue }: { name: string; value: string } +): React.ComponentType { + return forwardRef((props, ref) => { + const logPrefix = `toggleUserProperty ${propertyName} -|`; + try { + const user = authStore((state) => state.user); + const updateUserProperty = authStore((state) => state.updateUserProperty); + + if (!user) { + throw new Error("User loaded required"); + } + + // Resolve toggle value from props if needed + let resolvedToggleValue = resolveValue(toggleValue, props); + + const propertyValue = getNestedProperty(user, propertyName) || ""; + const propertyValueAsArray = + typeof propertyValue === "string" + ? propertyValue.split(",").map((item) => item.trim()) + : []; + + const handleClick = async (event: React.MouseEvent) => { + event.preventDefault(); + + log(logPrefix, { propertyName, resolvedToggleValue }); + + const newPropertyValueAsArray = [ + // Filter out the toggle value and empty values + ...propertyValueAsArray.filter( + (item) => item !== resolvedToggleValue && item.trim() !== "" + ), + // Add toggle value at the end if not included in current value + ...(propertyValueAsArray.includes(resolvedToggleValue) + ? [] + : [resolvedToggleValue]), + ].filter((item) => item.trim() !== ""); // Final filter to remove any empty values + + await updateUserProperty( + propertyName, + newPropertyValueAsArray.join(", ") + ); + + // Call original onClick if provided + if (props.onClick) { + props.onClick(event); + } + }; + + const active = propertyValueAsArray.includes(resolvedToggleValue); + + return ( + + ); + } catch (error) { + if (error instanceof Error) { + log(logPrefix, "Hiding component", error.message); + } else { + log(logPrefix, "Hiding component", error); + } + return null; + } + }); +} + +/** + * Shows component based on property comparison + * @param Component - The component to wrap + * @param options - Configuration + * @param options.name - Property name to check + * @param options.value - Value to compare against (can be "props.propertyName") + * @param options.compare - Comparison type: "equal" or "array-includes" + * @param options.flags - Additional flags like ["ignore-case"] + */ +export function showForUserProperty( + Component: React.ComponentType, + { + name: propertyName, + value, + compare: compareType = "equal", + flags = [], + }: ComparePropertyOptions +): React.ComponentType { + return forwardRef((props, ref) => { + const logPrefix = `showForUserProperty ${propertyName} -|`; + + try { + const user = authStore((state) => state.user); + + if (!user) { + throw new Error("User loaded required"); + } + const propertyValue = getNestedProperty(user, propertyName); + const resolvedValue = resolveValue(value, props); + + log(logPrefix, { + propertyValue, + resolvedValue, + compareType, + }); + + if (!compare(propertyValue, resolvedValue, compareType, flags)) { + throw new Error("Match not found - hiding component"); + } + + log(logPrefix, "Match found - showing component"); + return ; + } catch (error) { + if (error instanceof Error) { + log(logPrefix, "Hiding component", error.message); + } else { + log(logPrefix, "Hiding component", error); + } + return null; + } + }); +} + +/** + * Sets component text to a user property value + * @param Component - The component to wrap + * @param options - Configuration + * @param options.name - Property name to display + */ +export function withUserProperty( + Component: React.ComponentType, + { name: propertyName }: PropertyOptions +): React.ComponentType { + return forwardRef((props, ref) => { + const logPrefix = `withUserProperty ${propertyName} -|`; + + try { + const user = authStore((state) => state.user); + + if (!user) { + throw new Error("User loaded required"); + } + + let propertyValue = getNestedProperty(user, propertyName); + + if (typeof propertyValue !== "string") { + throw new Error("Not a string"); + } + + log(logPrefix, { propertyValue }); + return ; + } catch (error) { + if (error instanceof Error) { + log(logPrefix, "Hiding component", error.message); + } else { + log(logPrefix, "Hiding component", error); + } + return null; + } + }); +} + +/** + * Sets component background image to a user property value + * @param Component - The component to wrap + * @param options - Configuration + * @param options.name - Property name containing image URL + */ +export function withUserImageProperty( + Component: React.ComponentType, + { name: propertyName }: PropertyOptions +): React.ComponentType { + return forwardRef((props, ref) => { + const logPrefix = `withUserImageProperty ${propertyName} -|`; + + try { + const user = authStore((state) => state.user); + + if (!user) { + throw new Error("User loaded required"); + } + + const imageSrc = getNestedProperty(user, propertyName) as string; + + log(logPrefix, { imageSrc }); + + if (!imageSrc) { + // If no image from user property, use component's default or hide + if (props.background?.src) { + // Component has image set, use as fallback + log(logPrefix, "No user image, using component fallback"); + return ; + } else { + // Component has no image set, hide + log(logPrefix, "No user image and no component fallback, hiding"); + return null; + } + } + + log(logPrefix, "Setting image from user property"); + return ( + + ); + } catch (error) { + if (error instanceof Error) { + log(logPrefix, "Hiding component", error.message); + } else { + log(logPrefix, "Hiding component", error); + } + return null; + } + }); +} + +/** + * Hides component based on user property comparison + * @param Component - The component to wrap + * @param options - Configuration + * @param options.name - Property name to check + * @param options.value - Value to compare against (can be "props.propertyName") + * @param options.compare - Comparison type: "equal" or "array-includes" + * @param options.flags - Additional flags like ["ignore-case"] + */ +export function hideForUserPayloadProperty( + Component: React.ComponentType, + { + name: propertyName, + value, + compare: compareType = "equal", + flags = [], + }: ComparePropertyOptions +): React.ComponentType { + return forwardRef((props, ref) => { + const logPrefix = `hideForUserPayloadProperty ${propertyName} -|`; + + try { + const user = authStore((state) => state.user); + + if (!user) { + throw new Error("User loaded required"); + } + const propertyValue = getNestedProperty(user, propertyName); + const resolvedValue = resolveValue(value, props); + + log(logPrefix, { + propertyValue, + resolvedValue, + compareType, + }); + + if (compare(propertyValue, resolvedValue, compareType, flags)) { + throw new Error("Match found - hiding component"); + } + + log(logPrefix, "Match not found - showing component"); + return ; + } catch (error) { + if (error instanceof Error) { + log(logPrefix, "Hiding component", error.message); + } else { + log(logPrefix, "Hiding component", error); + } + return null; + } + }); +} diff --git a/src/framer/overrides/compare.tsx b/src/framer/overrides/utils.ts similarity index 64% rename from src/framer/overrides/compare.tsx rename to src/framer/overrides/utils.ts index b2b1d98..a55f22a 100644 --- a/src/framer/overrides/compare.tsx +++ b/src/framer/overrides/utils.ts @@ -1,3 +1,18 @@ +import { outsetaLog } from "../../outseta"; + +export type PropertyOptions = { + name: string; +}; + +export type ComparePropertyOptions = { + name: string; + value: any; + compare?: "equal" | "array-includes"; + flags?: "ignore-case"[]; +}; + +export const log = outsetaLog("framer.overrides"); + function arrayIncludes(array: any[], value: any, flags?: "ignore-case"[]) { if (flags?.includes("ignore-case")) { return array.some((item: any) => { @@ -40,3 +55,16 @@ export function compare( ); } } + +/** + * Resolves a value from props if it starts with "props." + * @param value - The value to resolve + * @param props - The component props + * @returns The resolved value + */ +export function resolveValue(value: any, props: any) { + if (typeof value === "string" && value.startsWith("props.")) { + return props[value.replace("props.", "")]; + } + return value; +} diff --git a/src/test/setup.ts b/src/test/setup.ts new file mode 100644 index 0000000..69f9e09 --- /dev/null +++ b/src/test/setup.ts @@ -0,0 +1,21 @@ +import "@testing-library/jest-dom"; +import { vi } from "vitest"; + +// Mock window.location for tests +Object.defineProperty(window, "location", { + value: { + host: "localhost:3000", + href: "http://localhost:3000", + }, + writable: true, +}); + +// Mock console methods to reduce noise in tests +global.console = { + ...console, + log: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), +}; diff --git a/vite.config.ts b/vite.config.ts index 1f42b24..24bf89c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,14 +4,19 @@ import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], + test: { + globals: true, + environment: "jsdom", + setupFiles: ["./src/test/setup.ts"], + }, build: { lib: { entry: { - "framer/overrides": resolve( + "framer/overrides": resolve(__dirname, "src/framer/overrides.tsx"), + "framer/hello-world": resolve( __dirname, - "src/framer/overrides/index.tsx" + "src/framer/overrides-hello-world.tsx" ), - "framer/test": resolve(__dirname, "src/framer/overrides/test.tsx"), }, formats: ["es"], },