diff --git a/.gitignore b/.gitignore
index 3802633..dd335f8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,7 +13,6 @@ npm-debug.log
data
settings.json
-scripts
logs
attachments
recipients.csv
diff --git a/scripts/package-lock.json b/scripts/package-lock.json
new file mode 100644
index 0000000..3a02f2a
--- /dev/null
+++ b/scripts/package-lock.json
@@ -0,0 +1,800 @@
+{
+ "name": "scripts",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "devDependencies": {
+ "playwright": "^1.61.1",
+ "ts-node": "^10.9.2",
+ "tsx": "^4.23.0",
+ "typescript": "^6.0.3"
+ }
+ },
+ "node_modules/@cspotcode/source-map-support": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@tsconfig/node10": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
+ "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node12": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+ "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node14": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+ "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node16": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
+ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "26.1.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz",
+ "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "undici-types": "~8.3.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.17.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.3.5",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
+ "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/create-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/diff": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
+ "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.1",
+ "@esbuild/android-arm": "0.28.1",
+ "@esbuild/android-arm64": "0.28.1",
+ "@esbuild/android-x64": "0.28.1",
+ "@esbuild/darwin-arm64": "0.28.1",
+ "@esbuild/darwin-x64": "0.28.1",
+ "@esbuild/freebsd-arm64": "0.28.1",
+ "@esbuild/freebsd-x64": "0.28.1",
+ "@esbuild/linux-arm": "0.28.1",
+ "@esbuild/linux-arm64": "0.28.1",
+ "@esbuild/linux-ia32": "0.28.1",
+ "@esbuild/linux-loong64": "0.28.1",
+ "@esbuild/linux-mips64el": "0.28.1",
+ "@esbuild/linux-ppc64": "0.28.1",
+ "@esbuild/linux-riscv64": "0.28.1",
+ "@esbuild/linux-s390x": "0.28.1",
+ "@esbuild/linux-x64": "0.28.1",
+ "@esbuild/netbsd-arm64": "0.28.1",
+ "@esbuild/netbsd-x64": "0.28.1",
+ "@esbuild/openbsd-arm64": "0.28.1",
+ "@esbuild/openbsd-x64": "0.28.1",
+ "@esbuild/openharmony-arm64": "0.28.1",
+ "@esbuild/sunos-x64": "0.28.1",
+ "@esbuild/win32-arm64": "0.28.1",
+ "@esbuild/win32-ia32": "0.28.1",
+ "@esbuild/win32-x64": "0.28.1"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/playwright": {
+ "version": "1.61.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
+ "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.61.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.61.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
+ "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/ts-node": {
+ "version": "10.9.2",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
+ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cspotcode/source-map-support": "^0.8.0",
+ "@tsconfig/node10": "^1.0.7",
+ "@tsconfig/node12": "^1.0.7",
+ "@tsconfig/node14": "^1.0.0",
+ "@tsconfig/node16": "^1.0.2",
+ "acorn": "^8.4.1",
+ "acorn-walk": "^8.1.1",
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "v8-compile-cache-lib": "^3.0.1",
+ "yn": "3.1.1"
+ },
+ "bin": {
+ "ts-node": "dist/bin.js",
+ "ts-node-cwd": "dist/bin-cwd.js",
+ "ts-node-esm": "dist/bin-esm.js",
+ "ts-node-script": "dist/bin-script.js",
+ "ts-node-transpile-only": "dist/bin-transpile.js",
+ "ts-script": "dist/bin-script-deprecated.js"
+ },
+ "peerDependencies": {
+ "@swc/core": ">=1.2.50",
+ "@swc/wasm": ">=1.2.50",
+ "@types/node": "*",
+ "typescript": ">=2.7"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "@swc/wasm": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tsx": {
+ "version": "4.23.0",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz",
+ "integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.28.0"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/tsx/node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
+ "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/v8-compile-cache-lib": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ }
+ }
+}
diff --git a/scripts/package.json b/scripts/package.json
new file mode 100644
index 0000000..69832bb
--- /dev/null
+++ b/scripts/package.json
@@ -0,0 +1,8 @@
+{
+ "devDependencies": {
+ "playwright": "^1.61.1",
+ "ts-node": "^10.9.2",
+ "tsx": "^4.23.0",
+ "typescript": "^6.0.3"
+ }
+}
diff --git a/scripts/record.js b/scripts/record.js
new file mode 100644
index 0000000..6181bc7
--- /dev/null
+++ b/scripts/record.js
@@ -0,0 +1,222 @@
+const { chromium } = require('playwright');
+const fs = require('fs');
+const {
+ injectDemoVisuals,
+ announce,
+ clickWithFlourish,
+ typeSlowly,
+ zoomToElement,
+ resetZoom,
+ pause
+} = require('./utils');
+
+const BASE_URL = 'http://localhost:3000';
+const VIDEO_PATH = 'dist/full_screen.webm';
+
+async function main() {
+ console.log('Launching browser (Headless: false) and starting video recording...');
+ const browser = await chromium.launch({
+ slowMo: 40,
+ headless: false,
+ args: ['--start-maximized']
+ });
+
+ const context = await browser.newContext({
+ viewport: null,
+ recordVideo: {
+ dir: 'dist/'
+ }
+ });
+
+ const page = await context.newPage();
+ const video = page.video();
+
+ try {
+ console.log(`Navigating to ${BASE_URL}...`);
+ await page.goto(BASE_URL);
+ await injectDemoVisuals(page);
+ await pause(page, 1500);
+
+ console.log('Navigating to Templates and scrolling to the bottom...');
+ await announce(page, 'Exploring the Template Library...');
+ const templatesLink = page.locator('a[title="Templates"]');
+ await clickWithFlourish(page, templatesLink);
+ await page.waitForURL('**/templates*');
+ await pause(page, 1000);
+
+ await page.evaluate(() => {
+ const scrollables = Array.from(document.querySelectorAll('.custom-scrollbar, .overflow-y-auto'))
+ .filter(el => el.scrollHeight > el.clientHeight);
+ if (scrollables.length > 0) {
+ scrollables[0].scrollTo({ top: scrollables[0].scrollHeight, behavior: 'smooth' });
+ } else {
+ window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
+ }
+ });
+ await pause(page, 2000);
+
+ console.log('Navigating to Databases...');
+ await announce(page, 'Viewing Saved Databases...');
+ const databasesLink = page.locator('a[title="Databases"]');
+ await clickWithFlourish(page, databasesLink);
+ await page.waitForURL('**/databases*');
+ await pause(page, 1500);
+
+ console.log('Navigating to Settings to configure SMTP...');
+ await announce(page, 'Configuring a new SMTP Sender Account...');
+ const settingsLink = page.locator('a[title="Settings"]');
+ await clickWithFlourish(page, settingsLink);
+ await page.waitForURL('**/settings');
+ await pause(page, 1000);
+
+ const addAccountBtn = page.getByRole('button', { name: /Add.*Account/i }).first();
+ await clickWithFlourish(page, addAccountBtn);
+
+ console.log('Inputting SMTP credentials...');
+ await typeSlowly(page, page.locator('input[placeholder="e.g., Marketing Team"]'), 'Demo Marketing');
+ await typeSlowly(page, page.locator('input[placeholder="smtp.gmail.com"]'), 'smtp.gmail.com');
+
+ const portInput = page.locator('input[placeholder="587"]');
+ await portInput.fill('');
+ await typeSlowly(page, portInput, '587');
+
+ await typeSlowly(page, page.locator('input[placeholder="you@example.com"]'), 'acm.marketing@gmail.com');
+ await typeSlowly(page, page.locator('input[placeholder="Enter password"]'), 'dummy_password_123');
+
+ const saveAccountBtn = page.getByRole('button', { name: 'Save Account' });
+ await clickWithFlourish(page, saveAccountBtn);
+ await pause(page, 1500);
+
+ console.log('Navigating to Campaigns...');
+ await announce(page, 'Checking existing campaigns...');
+ const campaignsLink = page.locator('a[title="Campaigns"]');
+ await clickWithFlourish(page, campaignsLink);
+ await page.waitForURL('**/campaigns*');
+ await pause(page, 1500);
+
+ console.log('Navigating to Templates to use a pre-built template...');
+ await announce(page, 'Using a pre-built template for our new campaign...');
+ await clickWithFlourish(page, templatesLink);
+ await page.waitForURL('**/templates*');
+
+ const targetCard = page.locator('.bg-surface-card').filter({ has: page.getByRole('heading', { name: 'Recruitment - Not Selected' }) }).first();
+ await targetCard.scrollIntoViewIfNeeded();
+ await targetCard.hover();
+ await pause(page, 500);
+
+ const useThisBtn = targetCard.getByRole('button', { name: 'Use this' });
+ console.log('Zooming in on the "Use this" button...');
+ await zoomToElement(page, useThisBtn, 1.4);
+ await clickWithFlourish(page, useThisBtn);
+ await pause(page, 1000);
+ await resetZoom(page);
+
+ console.log('Creating a new campaign from the template...');
+ await page.waitForURL('**/campaigns/new?templateId=*');
+ await announce(page, 'Creating a new campaign from the template...');
+ const campaignNameInput = page.locator('#campaignName');
+ await typeSlowly(page, campaignNameInput, 'Fall Recruitment Updates');
+
+ const createCampaignBtn = page.getByRole('button', { name: 'Create Campaign' });
+ await createCampaignBtn.scrollIntoViewIfNeeded();
+
+ await page.evaluate(() => {
+ const scrollables = Array.from(document.querySelectorAll('.custom-scrollbar, .overflow-y-auto'))
+ .filter(el => el.scrollHeight > el.clientHeight);
+ if (scrollables.length > 0) {
+ scrollables[0].scrollTo({ top: scrollables[0].scrollHeight, behavior: 'smooth' });
+ } else {
+ window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
+ }
+ });
+ await pause(page, 1000);
+
+ await clickWithFlourish(page, createCampaignBtn);
+
+ await page.waitForURL('**/campaigns/*');
+ await pause(page, 1500);
+
+ console.log('Toggling the recipients panel...');
+ await announce(page, 'Toggling the recipients panel...');
+ const recipientsBtn = page.getByRole('button', { name: /Recipients/ });
+ await clickWithFlourish(page, recipientsBtn);
+ await pause(page, 1000);
+ await clickWithFlourish(page, recipientsBtn);
+ await pause(page, 1000);
+
+ console.log('Checking the live logs for preflight status...');
+ await announce(page, 'Checking the live logs for preflight status...');
+ const liveLogsHeader = page.locator('h3', { hasText: 'Live Logs' });
+ await clickWithFlourish(page, liveLogsHeader);
+ await pause(page, 2000);
+
+ console.log('Closing the live logs...');
+ await clickWithFlourish(page, liveLogsHeader);
+ await pause(page, 1000);
+
+ console.log('Importing recipients from a saved database...');
+ await announce(page, 'Importing recipients from our saved database...');
+ const moreOptionsBtn = page.getByTitle('More Options');
+ await clickWithFlourish(page, moreOptionsBtn);
+ const importDbBtn = page.getByText('Import from Database');
+ await clickWithFlourish(page, importDbBtn);
+
+ const dbSelect = page.locator('select');
+ await dbSelect.waitFor({ state: 'visible' });
+ const dbOptionValue = await page.locator('select option', { hasText: 'AISSMS IOIT ACM Student Chapter Contacts' }).getAttribute('value');
+ await dbSelect.selectOption(dbOptionValue);
+ await pause(page, 500);
+
+ const importRecordsBtn = page.getByRole('button', { name: 'Import Records' });
+ await clickWithFlourish(page, importRecordsBtn);
+ await pause(page, 1500);
+
+ console.log('Opening recipients and previewing emails...');
+ await clickWithFlourish(page, recipientsBtn);
+ await pause(page, 1000);
+
+ await announce(page, 'Previewing the personalized email...');
+ const previewBtn = page.locator('table tbody tr').first().locator('button[aria-label="Preview Email"]');
+ await clickWithFlourish(page, previewBtn);
+ await pause(page, 2000);
+
+ console.log('Scrolling through recipient previews...');
+ await announce(page, 'Scrolling through recipient previews...');
+ const nextBtn = page.locator('button[aria-label="Next Recipient"]');
+ await clickWithFlourish(page, nextBtn);
+ await pause(page, 1500);
+ await clickWithFlourish(page, nextBtn);
+ await pause(page, 2000);
+
+ console.log('Closing preview modal...');
+ const closeModalBtn = page.getByLabel('Close modal');
+ await clickWithFlourish(page, closeModalBtn);
+ await pause(page, 1000);
+
+ console.log('Opening the confirmation modal...');
+ await announce(page, 'Ready to dispatch! Reviewing the final summary...', 3000);
+ const startSendingBtn = page.getByRole('button', { name: 'Start Sending' });
+ await clickWithFlourish(page, startSendingBtn);
+ await pause(page, 4500);
+
+ console.log('Demo complete! Closing browser and saving video to ' + VIDEO_PATH);
+ } catch (error) {
+ console.error('An error occurred during the demo:', error);
+ throw error;
+ } finally {
+ await context.close();
+ await browser.close();
+
+ if (video) {
+ const tempVideoPath = await video.path();
+ if (fs.existsSync(tempVideoPath)) {
+ fs.renameSync(tempVideoPath, VIDEO_PATH);
+ }
+ }
+ }
+}
+
+main().catch((err) => {
+ console.error('Fatal error:', err);
+ process.exit(1);
+});
diff --git a/scripts/utils.js b/scripts/utils.js
new file mode 100644
index 0000000..8d7ab2e
--- /dev/null
+++ b/scripts/utils.js
@@ -0,0 +1,265 @@
+async function injectDemoVisuals(page) {
+ await page.addStyleTag({
+ content: `
+ #__demo_cursor {
+ position: fixed;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ background: rgba(255, 59, 48, 0.55);
+ border: 2px solid rgba(255, 59, 48, 0.9);
+ z-index: 2147483647;
+ pointer-events: none;
+ transform: translate(-50%, -50%);
+ transition: left 0.35s cubic-bezier(0.22, 1, 0.36, 1), top 0.35s cubic-bezier(0.22, 1, 0.36, 1);
+ }
+
+ #__demo_zoom_wrapper {
+ transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
+ transform-origin: 0 0;
+ will-change: transform;
+ }
+
+ #__demo_click_ring {
+ position: fixed;
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ border: 2px solid rgba(255, 59, 48, 0.9);
+ z-index: 2147483646;
+ pointer-events: none;
+ transform: translate(-50%, -50%) scale(0);
+ opacity: 0;
+ }
+
+ #__demo_click_ring.pulse {
+ animation: __demo_pulse 0.5s ease-out;
+ }
+
+ @keyframes __demo_pulse {
+ 0% { transform: translate(-50%, -50%) scale(0.4); opacity: 0.9; }
+ 100% { transform: translate(-50%, -50%) scale(1.8); opacity: 0; }
+ }
+
+ #__demo_caption_box {
+ position: fixed;
+ bottom: 80px;
+ left: 50%;
+ transform: translateX(-50%) translateY(20px);
+ background: #ffffff;
+ color: #000000;
+ padding: 16px 36px;
+ border-radius: 8px;
+ font-family: system-ui, -apple-system, sans-serif;
+ font-size: 22px;
+ font-weight: 600;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.15);
+ z-index: 2147483647;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 0.4s ease, transform 0.4s ease;
+ text-align: center;
+ max-width: 80%;
+ white-space: nowrap;
+ }
+
+ #__demo_caption_box.visible {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ }
+ `,
+ });
+
+ await page.evaluate(() => {
+ const cursor = document.createElement('div');
+ cursor.id = '__demo_cursor';
+ document.body.appendChild(cursor);
+
+ const ring = document.createElement('div');
+ ring.id = '__demo_click_ring';
+ document.body.appendChild(ring);
+
+ const caption = document.createElement('div');
+ caption.id = '__demo_caption_box';
+ document.body.appendChild(caption);
+
+ const wrapper = document.createElement('div');
+ wrapper.id = '__demo_zoom_wrapper';
+
+ while (document.body.firstChild) {
+ if (
+ document.body.firstChild === cursor ||
+ document.body.firstChild === ring ||
+ document.body.firstChild === caption
+ ) {
+ break;
+ }
+
+ wrapper.appendChild(document.body.firstChild);
+ }
+
+ document.body.insertBefore(wrapper, cursor);
+ });
+}
+
+async function announce(page, text, durationMs = 3000) {
+ await page.evaluate(
+ ({ msg, duration }) => {
+ const box = document.getElementById('__demo_caption_box');
+
+ if (!box) return;
+
+ if (window.__demo_caption_timeout) {
+ clearTimeout(window.__demo_caption_timeout);
+ }
+
+ box.innerHTML = `
+
+ ${msg}
+ `;
+
+ box.classList.add('visible');
+
+ requestAnimationFrame(() => {
+ const rect = document.getElementById('__demo_caption_rect');
+
+ if (!rect) return;
+
+ const length = rect.getTotalLength();
+
+ rect.style.strokeDasharray = length;
+ rect.style.strokeDashoffset = length;
+ rect.style.transition = `stroke-dashoffset ${duration}ms linear`;
+
+ void rect.getBoundingClientRect();
+
+ rect.style.strokeDashoffset = '0';
+ });
+
+ window.__demo_caption_timeout = setTimeout(() => {
+ box.classList.remove('visible');
+ }, duration);
+ },
+ { msg: text, duration: durationMs }
+ );
+
+ await page.waitForTimeout(200);
+}
+
+async function moveCursorTo(page, x, y, steps = 30) {
+ await page.evaluate(
+ ({ x, y }) => {
+ const cursor = document.getElementById('__demo_cursor');
+
+ if (cursor) {
+ cursor.style.left = `${x}px`;
+ cursor.style.top = `${y}px`;
+ }
+ },
+ { x, y }
+ );
+
+ await page.mouse.move(x, y, { steps });
+ await page.waitForTimeout(380);
+}
+
+async function clickWithFlourish(page, locator) {
+ const box = await locator.boundingBox();
+
+ if (!box) {
+ throw new Error('Element not visible for clickWithFlourish');
+ }
+
+ const x = box.x + box.width / 2;
+ const y = box.y + box.height / 2;
+
+ await moveCursorTo(page, x, y);
+
+ await page.evaluate(
+ ({ x, y }) => {
+ const ring = document.getElementById('__demo_click_ring');
+
+ if (ring) {
+ ring.style.left = `${x}px`;
+ ring.style.top = `${y}px`;
+ ring.classList.remove('pulse');
+ void ring.offsetWidth;
+ ring.classList.add('pulse');
+ }
+ },
+ { x, y }
+ );
+
+ await page.waitForTimeout(150);
+ await locator.click();
+}
+
+async function typeSlowly(page, locator, text, minDelay = 55, maxDelay = 130) {
+ const box = await locator.boundingBox();
+
+ if (box) {
+ await moveCursorTo(page, box.x + box.width / 2, box.y + box.height / 2);
+ }
+
+ await locator.click();
+
+ for (const char of text) {
+ await locator.pressSequentially(char, { delay: 0 });
+ const delay = minDelay + Math.random() * (maxDelay - minDelay);
+ await page.waitForTimeout(delay);
+ }
+}
+
+async function zoomToElement(page, locator, scale = 1.6) {
+ const box = await locator.boundingBox();
+
+ if (!box) {
+ throw new Error('Element not visible for zoomToElement');
+ }
+
+ const x = box.x + box.width / 2;
+ const y = box.y + box.height / 2;
+
+ await page.evaluate(
+ ({ x, y, scale }) => {
+ const wrapper = document.getElementById('__demo_zoom_wrapper');
+
+ if (wrapper) {
+ wrapper.style.transformOrigin = `${x}px ${y}px`;
+ wrapper.style.transform = `scale(${scale})`;
+ }
+ },
+ { x, y, scale }
+ );
+
+ await page.waitForTimeout(650);
+}
+
+async function resetZoom(page) {
+ await page.evaluate(() => {
+ const wrapper = document.getElementById('__demo_zoom_wrapper');
+
+ if (wrapper) {
+ wrapper.style.transform = 'scale(1)';
+ }
+ });
+
+ await page.waitForTimeout(650);
+}
+
+async function pause(page, ms = 1200) {
+ await page.waitForTimeout(ms);
+}
+
+module.exports = {
+ injectDemoVisuals,
+ announce,
+ moveCursorTo,
+ clickWithFlourish,
+ typeSlowly,
+ zoomToElement,
+ resetZoom,
+ pause,
+};