diff --git a/bun.lock b/bun.lock index 04e34b8..bd6f6c4 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "welcome-app", "devDependencies": { "@playwright/test": "^1.60.0", - "@screenly/edge-apps": "^1.2.1", + "@screenly/edge-apps": "^1.5.0", "@types/bun": "^1.3.14", "@types/jsdom": "^28.0.3", "bun-types": "^1.3.14", @@ -44,7 +44,7 @@ "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], - "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], @@ -120,53 +120,57 @@ "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.2" }, "os": "darwin", "cpu": "arm64" }, "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg=="], - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.2" }, "os": "darwin", "cpu": "x64" }, "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w=="], - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "os": "freebsd" }, "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg=="], - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg=="], - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw=="], - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ=="], - "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA=="], - "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw=="], - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.2", "", { "os": "linux", "cpu": "none" }, "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w=="], - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ=="], - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w=="], - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw=="], - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ=="], - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.2" }, "os": "linux", "cpu": "arm" }, "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA=="], - "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ=="], - "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.2" }, "os": "linux", "cpu": "ppc64" }, "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA=="], - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.2" }, "os": "linux", "cpu": "none" }, "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ=="], - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.2" }, "os": "linux", "cpu": "s390x" }, "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw=="], - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA=="], - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w=="], - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg=="], - "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="], - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "cpu": "none" }, "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q=="], - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -414,7 +418,7 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], - "@screenly/edge-apps": ["@screenly/edge-apps@1.2.1", "", { "dependencies": { "@eslint/js": "^10.0.1", "@photostructure/tz-lookup": "^11.5.0", "@sentry/browser": "^10.59.0", "@tailwindcss/vite": "^4.2.1", "country-locale-map": "^1.9.12", "dayjs": "^1.11.19", "eslint": "^10.0.3", "jiti": "^2.6.1", "jsdom": "^28.1.0", "offline-geocode-city": "^1.0.2", "panic-overlay": "^1.0.51", "sharp": "^0.34.5", "tailwindcss": "^4.2.1", "typescript": "^5.9.3", "typescript-eslint": "^8.57.0", "vite": "^8.1.2", "yaml": "^2.8.2" }, "peerDependencies": { "@playwright/test": "^1.58.0" }, "optionalPeers": ["@playwright/test"], "bin": { "edge-apps-scripts": "bin/edge-apps-scripts.js" } }, "sha512-9CTA8eaYtVlrACHWU9uhml3Yf7hIgc6mELmXhqDdHKDC2fmrgma3sAOpvr0sBjTJr37QRvpeqUjlZ9BJlwT0nQ=="], + "@screenly/edge-apps": ["@screenly/edge-apps@1.5.0", "", { "dependencies": { "@eslint/js": "^10.0.1", "@photostructure/tz-lookup": "^11.5.0", "@sentry/browser": "^10.59.0", "@tailwindcss/vite": "^4.2.1", "country-locale-map": "^1.9.12", "dayjs": "^1.11.19", "eslint": "^10.0.3", "jiti": "^2.6.1", "jsdom": "^28.1.0", "offline-geocode-city": "^1.0.2", "panic-overlay": "^1.0.51", "sharp": "^0.35.3", "tailwindcss": "^4.2.1", "typescript": "^5.9.3", "typescript-eslint": "^8.57.0", "vite": "^8.1.2", "yaml": "^2.8.2" }, "peerDependencies": { "@playwright/test": "^1.58.0" }, "optionalPeers": ["@playwright/test"], "bin": { "edge-apps-scripts": "bin/edge-apps-scripts.js" } }, "sha512-yEO2KA61p6FHTYQUuw1ffyAcDDoxhiCrj8iQ2evAw9OCid2yYQ5Z2zJbAWebGfhxSo2NzfUEcC9O7rZd+VS7OA=="], "@sentry/browser": ["@sentry/browser@10.67.0", "", { "dependencies": { "@sentry/browser-utils": "10.67.0", "@sentry/conventions": "^0.16.0", "@sentry/core": "10.67.0", "@sentry/feedback": "10.67.0", "@sentry/replay": "10.67.0", "@sentry/replay-canvas": "10.67.0" } }, "sha512-/ZhsAvte4rYhg0A0RtSFFgAgXhyMOfQIeOAfMfptN+X6IVSYOfkA9jtrP+Ej4+6vlaUFWRir1HweF56y63dEEA=="], @@ -874,11 +878,11 @@ "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], - "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], - "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + "sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" } }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -976,9 +980,19 @@ "@jsheaven/easybuild/typescript": ["typescript@4.9.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g=="], + "@parcel/core/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "@parcel/node-resolver-core/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "@parcel/package-manager/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "@parcel/source-map/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], - "@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], + "@parcel/transformer-babel/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "@parcel/transformer-js/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "@parcel/transformer-postcss/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "@screenly/edge-apps/jsdom": ["jsdom@28.1.0", "", { "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.8.1", "@bramus/specificity": "^2.4.2", "@exodus/bytes": "^1.11.0", "cssstyle": "^6.0.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "undici": "^7.21.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug=="], @@ -1000,6 +1014,8 @@ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "@typescript-eslint/typescript-estree/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], diff --git a/e2e/screenshots.spec.ts b/e2e/screenshots.spec.ts index 5886f26..885f357 100644 --- a/e2e/screenshots.spec.ts +++ b/e2e/screenshots.spec.ts @@ -8,35 +8,46 @@ import { } from '@screenly/edge-apps/test/screenshots' import path from 'path' -const { screenlyJsContent } = createMockScreenlyForScreenshots( - { coordinates: [40.7128, -74.006], location: 'New York, NY' }, - { - enable_analytics: 'false', - override_locale: 'en', - override_timezone: 'America/New_York', - welcome_heading: 'Welcome', - welcome_message: 'to the team, Sam!', - }, -) - -for (const { width, height } of RESOLUTIONS) { - test(`screenshot ${width}x${height}`, async ({ browser }) => { - const screenshotsDir = getScreenshotsDir() - - const context = await browser.newContext({ viewport: { width, height } }) - const page = await context.newPage() - - await setupClockMock(page) - await setupScreenlyJsMock(page, screenlyJsContent) - - await page.goto('/') - await page.waitForLoadState('networkidle') - - await page.screenshot({ - path: path.join(screenshotsDir, `${width}x${height}.png`), - fullPage: false, - }) +const VARIANTS = [ + { prefix: '', mode: 'lobby' }, + { prefix: 'poster-', mode: 'poster' }, +] + +for (const { prefix, mode } of VARIANTS) { + const { screenlyJsContent } = createMockScreenlyForScreenshots( + { coordinates: [40.7128, -74.006], location: 'New York, NY' }, + { + enable_analytics: 'false', + mode, + override_locale: 'en', + override_timezone: 'America/New_York', + welcome_heading: 'Welcome', + welcome_message: 'to the team, Sam!', + }, + ) + + for (const { width, height } of RESOLUTIONS) { + test(`screenshot ${prefix}${width}x${height}`, async ({ browser }) => { + const screenshotsDir = getScreenshotsDir() + + const context = await browser.newContext({ + viewport: { width, height }, + reducedMotion: 'reduce', + }) + const page = await context.newPage() - await context.close() - }) + await setupClockMock(page) + await setupScreenlyJsMock(page, screenlyJsContent) + + await page.goto('/') + await page.waitForLoadState('networkidle') + + await page.screenshot({ + path: path.join(screenshotsDir, `${prefix}${width}x${height}.png`), + fullPage: false, + }) + + await context.close() + }) + } } diff --git a/index.html b/index.html index 8f734fa..fa8f0d5 100644 --- a/index.html +++ b/index.html @@ -13,13 +13,17 @@ orientation="auto" padding="0" > -
- -
+
+
+ +

+
+
+

-
-
+ + diff --git a/package.json b/package.json index 13bee53..9305a4a 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "prettier": "./.prettierrc.json", "devDependencies": { "@playwright/test": "^1.60.0", - "@screenly/edge-apps": "^1.2.1", + "@screenly/edge-apps": "^1.5.0", "@types/bun": "^1.3.14", "@types/jsdom": "^28.0.3", "bun-types": "^1.3.14", diff --git a/screenly.yml b/screenly.yml index fc74193..105fa50 100644 --- a/screenly.yml +++ b/screenly.yml @@ -15,6 +15,21 @@ settings: optional: true help_text: Enable or disable Sentry and Google Analytics integrations. is_global: true + mode: + type: string + default_value: lobby + title: Mode + optional: true + help_text: + properties: + help_text: Choose the layout for where the screen is placed. + options: + - label: Lobby (logo and date header) + value: lobby + - label: Poster (message only) + value: poster + type: select + schema_version: 1 override_locale: type: string default_value: en diff --git a/screenly_qc.yml b/screenly_qc.yml index 2783ceb..45918de 100644 --- a/screenly_qc.yml +++ b/screenly_qc.yml @@ -15,6 +15,21 @@ settings: optional: true help_text: Enable or disable Sentry and Google Analytics integrations. is_global: true + mode: + type: string + default_value: lobby + title: Mode + optional: true + help_text: + properties: + help_text: Choose the layout for where the screen is placed. + options: + - label: Lobby (logo and date header) + value: lobby + - label: Poster (message only) + value: poster + type: select + schema_version: 1 override_locale: type: string default_value: en diff --git a/screenshots/1080x1920.webp b/screenshots/1080x1920.webp index a64b9e9..9851cfd 100644 Binary files a/screenshots/1080x1920.webp and b/screenshots/1080x1920.webp differ diff --git a/screenshots/1280x720.webp b/screenshots/1280x720.webp index b4a083d..5c0fa91 100644 Binary files a/screenshots/1280x720.webp and b/screenshots/1280x720.webp differ diff --git a/screenshots/1920x1080.webp b/screenshots/1920x1080.webp index 2bc4f1a..3cf046b 100644 Binary files a/screenshots/1920x1080.webp and b/screenshots/1920x1080.webp differ diff --git a/screenshots/2160x3840.webp b/screenshots/2160x3840.webp index f42557b..25622aa 100644 Binary files a/screenshots/2160x3840.webp and b/screenshots/2160x3840.webp differ diff --git a/screenshots/2160x4096.webp b/screenshots/2160x4096.webp index 17a4cd4..f578c7a 100644 Binary files a/screenshots/2160x4096.webp and b/screenshots/2160x4096.webp differ diff --git a/screenshots/3840x2160.webp b/screenshots/3840x2160.webp index f46bd14..d3c52ad 100644 Binary files a/screenshots/3840x2160.webp and b/screenshots/3840x2160.webp differ diff --git a/screenshots/4096x2160.webp b/screenshots/4096x2160.webp index f44203f..732a3af 100644 Binary files a/screenshots/4096x2160.webp and b/screenshots/4096x2160.webp differ diff --git a/screenshots/480x800.webp b/screenshots/480x800.webp index 1d4aa89..3769012 100644 Binary files a/screenshots/480x800.webp and b/screenshots/480x800.webp differ diff --git a/screenshots/720x1280.webp b/screenshots/720x1280.webp index 496b148..f1e18d8 100644 Binary files a/screenshots/720x1280.webp and b/screenshots/720x1280.webp differ diff --git a/screenshots/800x480.webp b/screenshots/800x480.webp index 5c04faf..e580317 100644 Binary files a/screenshots/800x480.webp and b/screenshots/800x480.webp differ diff --git a/screenshots/poster-1080x1920.webp b/screenshots/poster-1080x1920.webp new file mode 100644 index 0000000..475231a Binary files /dev/null and b/screenshots/poster-1080x1920.webp differ diff --git a/screenshots/poster-1280x720.webp b/screenshots/poster-1280x720.webp new file mode 100644 index 0000000..aaacc13 Binary files /dev/null and b/screenshots/poster-1280x720.webp differ diff --git a/screenshots/poster-1920x1080.webp b/screenshots/poster-1920x1080.webp new file mode 100644 index 0000000..a498872 Binary files /dev/null and b/screenshots/poster-1920x1080.webp differ diff --git a/screenshots/poster-2160x3840.webp b/screenshots/poster-2160x3840.webp new file mode 100644 index 0000000..c0f4ac6 Binary files /dev/null and b/screenshots/poster-2160x3840.webp differ diff --git a/screenshots/poster-2160x4096.webp b/screenshots/poster-2160x4096.webp new file mode 100644 index 0000000..45b1427 Binary files /dev/null and b/screenshots/poster-2160x4096.webp differ diff --git a/screenshots/poster-3840x2160.webp b/screenshots/poster-3840x2160.webp new file mode 100644 index 0000000..c85615e Binary files /dev/null and b/screenshots/poster-3840x2160.webp differ diff --git a/screenshots/poster-4096x2160.webp b/screenshots/poster-4096x2160.webp new file mode 100644 index 0000000..9f00cef Binary files /dev/null and b/screenshots/poster-4096x2160.webp differ diff --git a/screenshots/poster-480x800.webp b/screenshots/poster-480x800.webp new file mode 100644 index 0000000..f32c318 Binary files /dev/null and b/screenshots/poster-480x800.webp differ diff --git a/screenshots/poster-720x1280.webp b/screenshots/poster-720x1280.webp new file mode 100644 index 0000000..b38c9b3 Binary files /dev/null and b/screenshots/poster-720x1280.webp differ diff --git a/screenshots/poster-800x480.webp b/screenshots/poster-800x480.webp new file mode 100644 index 0000000..e42cd0e Binary files /dev/null and b/screenshots/poster-800x480.webp differ diff --git a/src/app.test.ts b/src/app.test.ts index 0b4669b..e9db541 100644 --- a/src/app.test.ts +++ b/src/app.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect, beforeEach, afterEach } from 'bun:test' import { setupScreenlyMock, resetScreenlyMock } from '@screenly/edge-apps/test' -import init from './app' +import init, { getHeadingSizeClass } from './app' describe('Welcome App', () => { beforeEach(() => { @@ -57,4 +57,31 @@ describe('Welcome App', () => { 'to the team', ) }) + + test('steps a long heading down in size', () => { + setupScreenlyMock({}, { welcome_heading: 'Welcome to our headquarters' }) + init() + + const headingEl = document.querySelector('#welcome-heading') + expect(headingEl?.classList.contains('is-long')).toBe(true) + }) +}) + +describe('getHeadingSizeClass', () => { + test('returns no class for a short heading', () => { + expect(getHeadingSizeClass('Welcome')).toBe('') + }) + + test('returns is-long past 16 characters', () => { + expect(getHeadingSizeClass('a'.repeat(17))).toBe('is-long') + }) + + test('returns is-very-long past 34 characters', () => { + expect(getHeadingSizeClass('a'.repeat(35))).toBe('is-very-long') + }) + + test('treats the thresholds as exclusive', () => { + expect(getHeadingSizeClass('a'.repeat(16))).toBe('') + expect(getHeadingSizeClass('a'.repeat(34))).toBe('is-long') + }) }) diff --git a/src/app.ts b/src/app.ts index c8016f0..833039e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,5 +1,16 @@ import { getSettingWithDefault, signalReady } from '@screenly/edge-apps' +const LONG_HEADING_CHARS = 16 +const VERY_LONG_HEADING_CHARS = 34 + +// The greeting is customer free text; a long one steps down in size instead +// of wrapping off the canvas +export function getHeadingSizeClass(text: string): string { + if (text.length > VERY_LONG_HEADING_CHARS) return 'is-very-long' + if (text.length > LONG_HEADING_CHARS) return 'is-long' + return '' +} + export default function init() { const welcomeHeading = getSettingWithDefault( 'welcome_heading', @@ -12,6 +23,9 @@ export default function init() { const headingEl = document.querySelector('#welcome-heading')! + headingEl.className = ['welcome-heading', getHeadingSizeClass(welcomeHeading)] + .filter(Boolean) + .join(' ') headingEl.textContent = welcomeHeading const messageEl = diff --git a/src/css/style.css b/src/css/style.css index 99863b5..a508e73 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -1,97 +1,242 @@ @import '@screenly/edge-apps/styles'; -* { - box-sizing: border-box; +body { + --welcome-accent: var(--theme-color-primary, #972eff); + --welcome-canvas: #08080d; + --welcome-ink: #fff; + --welcome-ink-dim: rgb(255 255 255 / 55%); + --welcome-accent-ink: color-mix( + in srgb, + var(--welcome-accent) 65%, + var(--welcome-ink) + ); + --welcome-heading-size: 15rem; + --welcome-message-size: 4.25rem; + --welcome-wash-position: 50% -12%; + --welcome-glow-position: 50% 115%; + + color: var(--welcome-ink); + background-color: var(--welcome-canvas); + background-image: + radial-gradient( + ellipse 70% 55% at var(--welcome-wash-position), + color-mix(in srgb, var(--welcome-accent) 45%, transparent), + transparent + ), + radial-gradient( + ellipse 60% 45% at var(--welcome-glow-position), + color-mix(in srgb, var(--welcome-accent) 18%, transparent), + transparent + ); } -body { - margin: 0; - padding: 0; - overflow: hidden; - color: white; - font-family: 'Inter', system-ui, sans-serif; - background: url('/static/images/bg.webp') no-repeat center center; - background-size: cover; +body.is-light-brand { + --welcome-canvas: #f6f6f8; + --welcome-ink: #0a0a0f; + --welcome-ink-dim: rgb(10 10 15 / 58%); } -body::before { - content: ''; - position: fixed; - inset: 0; - z-index: var(--z-index-backdrop); - pointer-events: none; - background: rgba(0, 0, 0, 0.2); - backdrop-filter: blur(7px); +body.daypart-morning { + --welcome-wash-position: 12% 0%; + --welcome-glow-position: 100% 100%; +} + +body.daypart-afternoon { + --welcome-wash-position: 50% -12%; + --welcome-glow-position: 50% 115%; +} + +body.daypart-evening { + --welcome-wash-position: 88% 0%; + --welcome-glow-position: 0% 100%; } auto-scaler { - position: fixed; + position: relative; z-index: var(--z-index-content); } -#app { +.welcome { display: flex; flex-direction: column; + gap: 3rem; width: 100%; height: 100%; - padding: 1.25rem; - gap: 1rem; + padding: 3rem 3.5rem; } -.header { - flex-shrink: 0; +.welcome-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.welcome-logo { + max-width: 12rem; + max-height: 3.5rem; + object-fit: contain; +} + +.welcome-logo.is-hidden { + display: none; +} + +.welcome-time { + margin: 0 0 0 auto; + font-size: 2.75rem; + font-weight: 500; + font-variant-numeric: tabular-nums; } -.content { +.welcome-body { flex: 1; + min-height: 0; display: flex; flex-direction: column; justify-content: center; - padding-left: 22.0625rem; - gap: 0.5rem; + gap: 3rem; + padding: 0 6rem; +} + +.welcome-eyebrow { + display: flex; + align-items: center; + gap: 1.5rem; + margin: 0; + font-size: 1.75rem; + font-weight: 600; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--welcome-accent-ink); +} + +.welcome-eyebrow:empty { + display: none; +} + +.welcome-eyebrow::before { + content: ''; + flex-shrink: 0; + width: 3.5rem; + height: 0.25rem; + border-radius: 0.125rem; + background: var(--welcome-accent-ink); } .welcome-heading { - font-size: 11.25rem; - font-weight: 500; - letter-spacing: -0.055em; - line-height: 1; margin: 0; - text-shadow: - 0 0.125rem 0.5rem rgba(0, 0, 0, 0.3), - 0 0.0625rem 0.125rem rgba(0, 0, 0, 0.15); + font-size: calc(var(--welcome-heading-size) * var(--welcome-heading-step, 1)); + font-weight: 600; + line-height: 0.95; + letter-spacing: -0.035em; + overflow-wrap: anywhere; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; } +.welcome-heading.is-long { + --welcome-heading-step: 0.7; + + -webkit-line-clamp: 3; +} + +.welcome-heading.is-very-long { + --welcome-heading-step: 0.5; + + -webkit-line-clamp: 4; +} + .welcome-message { - font-size: 4rem; - font-weight: 500; - letter-spacing: -0.047em; - line-height: 1.2; margin: 0; - text-shadow: - 0 0.125rem 0.5rem rgba(0, 0, 0, 0.3), - 0 0.0625rem 0.125rem rgba(0, 0, 0, 0.15); + max-width: 36ch; + font-size: var(--welcome-message-size); + line-height: 1.35; + color: var(--welcome-ink-dim); display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; } +.welcome-eyebrow, +.welcome-heading, +.welcome-message { + animation: welcome-enter 0.9s cubic-bezier(0.2, 0.7, 0.2, 1) both; +} + +.welcome-heading { + animation-delay: 0.1s; +} + +.welcome-message { + animation-delay: 0.22s; +} + +@keyframes welcome-enter { + from { + opacity: 0; + transform: translateY(1.75rem); + } + + to { + opacity: 1; + transform: none; + } +} + +@media (prefers-reduced-motion: reduce) { + .welcome-eyebrow, + .welcome-heading, + .welcome-message { + animation: none; + } +} + +body.mode-poster { + --welcome-heading-size: 17rem; + --welcome-message-size: 5rem; +} + +body.mode-poster .welcome-header, +body.mode-poster .welcome-eyebrow { + display: none; +} + +body.mode-poster .welcome-body { + align-items: center; + text-align: center; +} + @media (orientation: portrait) { - .content { - padding-left: 4rem; - padding-right: 4rem; + body { + --welcome-heading-size: 10rem; + --welcome-message-size: 3.5rem; + } + + .welcome { + padding: 3.5rem 3rem; + } + + .welcome-body { + padding: 0 2rem; + } + + .welcome-time { + font-size: 2.5rem; + } + + .welcome-eyebrow { + font-size: 1.6rem; } .welcome-heading { - font-size: 7.5rem; + -webkit-line-clamp: 3; } - .welcome-message { - font-size: 2.75rem; + body.mode-poster { + --welcome-heading-size: 12rem; + --welcome-message-size: 4rem; } } diff --git a/src/daypart.test.ts b/src/daypart.test.ts new file mode 100644 index 0000000..e2e923b --- /dev/null +++ b/src/daypart.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, test } from 'bun:test' +import { getDaypart, getGreeting } from './daypart' + +describe('getDaypart', () => { + test('morning runs from 05:00 to 11:59', () => { + expect(getDaypart(5)).toBe('morning') + expect(getDaypart(11)).toBe('morning') + }) + + test('afternoon runs from 12:00 to 17:59', () => { + expect(getDaypart(12)).toBe('afternoon') + expect(getDaypart(17)).toBe('afternoon') + }) + + test('evening covers the night', () => { + expect(getDaypart(18)).toBe('evening') + expect(getDaypart(23)).toBe('evening') + expect(getDaypart(0)).toBe('evening') + expect(getDaypart(4)).toBe('evening') + }) +}) + +describe('getGreeting', () => { + test('greets in the locale language', () => { + expect(getGreeting('en', 'morning')).toBe('Good morning') + expect(getGreeting('fr', 'evening')).toBe('Bonsoir') + expect(getGreeting('de', 'afternoon')).toBe('Guten Tag') + }) + + test('matches the language regardless of region or casing', () => { + expect(getGreeting('en_US', 'morning')).toBe('Good morning') + expect(getGreeting('fr-FR', 'morning')).toBe('Bonjour') + expect(getGreeting('ES', 'evening')).toBe('Buenas noches') + }) + + test('returns empty for a language it does not know', () => { + expect(getGreeting('ja', 'morning')).toBe('') + expect(getGreeting('', 'morning')).toBe('') + }) +}) diff --git a/src/daypart.ts b/src/daypart.ts new file mode 100644 index 0000000..006a8d5 --- /dev/null +++ b/src/daypart.ts @@ -0,0 +1,39 @@ +export const DAYPARTS = ['morning', 'afternoon', 'evening'] as const + +export type Daypart = (typeof DAYPARTS)[number] + +const GREETINGS: Record> = { + en: { + morning: 'Good morning', + afternoon: 'Good afternoon', + evening: 'Good evening', + }, + fr: { + morning: 'Bonjour', + afternoon: 'Bon après-midi', + evening: 'Bonsoir', + }, + de: { + morning: 'Guten Morgen', + afternoon: 'Guten Tag', + evening: 'Guten Abend', + }, + es: { + morning: 'Buenos días', + afternoon: 'Buenas tardes', + evening: 'Buenas noches', + }, +} + +export function getDaypart(hour: number): Daypart { + if (hour >= 5 && hour < 12) return 'morning' + if (hour >= 12 && hour < 18) return 'afternoon' + return 'evening' +} + +// The dictionary is deliberately small; an unknown locale drops the greeting +// rather than showing the wrong language +export function getGreeting(locale: string, daypart: Daypart): string { + const language = locale.toLowerCase().split(/[-_]/)[0] + return GREETINGS[language]?.[daypart] ?? '' +} diff --git a/src/main.ts b/src/main.ts index 3576020..f96ebb4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,105 @@ import './css/style.css' + +// Side-effect import: registers as a custom element import '@screenly/edge-apps/components' -import { setupErrorHandling } from '@screenly/edge-apps' +import { + formatLocalizedDate, + formatTime, + getLocale, + getTimeZone, + isLightColor, + setupBrandingLogo, + setupErrorHandling, + setupTheme, +} from '@screenly/edge-apps' import init from './app' +import { DAYPARTS, getDaypart, getGreeting } from './daypart' +import { getWelcomeMode } from './mode' + +const CLOCK_UPDATE_INTERVAL_MS = 1000 +const DAYPART_UPDATE_INTERVAL_MS = 60 * 1000 + +function getLocalHour(timezone: string): number { + return Number(formatTime(new Date(), 'en', timezone, { hour12: false }).hour) +} + +function applyDaypart(timezone: string) { + const daypart = getDaypart(getLocalHour(timezone)) + document.body.classList.remove(...DAYPARTS.map((part) => `daypart-${part}`)) + document.body.classList.add(`daypart-${daypart}`) +} + +function setupClock(locale: string, timezone: string) { + const timeEl = document.querySelector('[data-time]') + if (!timeEl) return + + const updateClock = () => { + const { hour, minute, dayPeriod } = formatTime(new Date(), locale, timezone) + timeEl.textContent = dayPeriod + ? `${hour}:${minute} ${dayPeriod}` + : `${hour}:${minute}` + } + + updateClock() + setInterval(updateClock, CLOCK_UPDATE_INTERVAL_MS) +} + +function setupEyebrow(locale: string, timezone: string) { + const eyebrowEl = document.querySelector('[data-eyebrow]') + if (!eyebrowEl) return + + const updateEyebrow = () => { + const now = new Date() + // No year: a lobby screen only ever shows today + const date = formatLocalizedDate(now, locale, { + weekday: 'long', + day: 'numeric', + month: 'long', + year: undefined, + timeZone: timezone, + }) + const greeting = getGreeting(locale, getDaypart(getLocalHour(timezone))) + eyebrowEl.textContent = greeting ? `${greeting} · ${date}` : date + } -document.addEventListener('DOMContentLoaded', () => { + updateEyebrow() + setInterval(updateEyebrow, DAYPART_UPDATE_INTERVAL_MS) +} + +async function setupLogo() { + const logoEl = document.querySelector('[data-logo]') + if (!logoEl) return + + logoEl.onerror = () => logoEl.classList.add('is-hidden') + try { + logoEl.src = await setupBrandingLogo() + logoEl.classList.remove('is-hidden') + } catch { + // Keep the logo hidden; an unattended screen must not show a broken image + } +} + +document.addEventListener('DOMContentLoaded', async () => { setupErrorHandling() + + const { primary } = setupTheme() + document.body.classList.toggle('is-light-brand', isLightColor(primary)) + + const mode = getWelcomeMode() + document.body.classList.add(`mode-${mode}`) + + const locale = await getLocale() + const timezone = await getTimeZone() + + applyDaypart(timezone) + setInterval(() => applyDaypart(timezone), DAYPART_UPDATE_INTERVAL_MS) + + // The poster mode never shows the header or eyebrow, so skip wiring them + if (mode === 'lobby') { + setupClock(locale, timezone) + setupEyebrow(locale, timezone) + await setupLogo() + } + init() }) diff --git a/src/mode.test.ts b/src/mode.test.ts new file mode 100644 index 0000000..2ec7fa3 --- /dev/null +++ b/src/mode.test.ts @@ -0,0 +1,21 @@ +import '@screenly/edge-apps/test' +import { describe, expect, test } from 'bun:test' +import { DEFAULT_MODE, resolveMode } from './mode' + +describe('resolveMode', () => { + test('returns known modes exactly', () => { + expect(resolveMode('lobby')).toBe('lobby') + expect(resolveMode('poster')).toBe('poster') + }) + + test('normalises casing and whitespace', () => { + expect(resolveMode(' Poster ')).toBe('poster') + expect(resolveMode('LOBBY')).toBe('lobby') + }) + + test('falls back to the default for unknown values', () => { + expect(resolveMode('banner')).toBe(DEFAULT_MODE) + expect(resolveMode('')).toBe(DEFAULT_MODE) + expect(resolveMode(undefined)).toBe(DEFAULT_MODE) + }) +}) diff --git a/src/mode.ts b/src/mode.ts new file mode 100644 index 0000000..a4ccdb9 --- /dev/null +++ b/src/mode.ts @@ -0,0 +1,18 @@ +import { getSettingWithDefault } from '@screenly/edge-apps' + +export const MODES = ['lobby', 'poster'] as const + +export type WelcomeMode = (typeof MODES)[number] + +export const DEFAULT_MODE: WelcomeMode = 'lobby' + +// Settings are free text at the API level, so an instance can hold a value +// this build has never heard of +export function resolveMode(value: string | undefined): WelcomeMode { + const normalized = value?.trim().toLowerCase() as WelcomeMode + return MODES.includes(normalized) ? normalized : DEFAULT_MODE +} + +export function getWelcomeMode(): WelcomeMode { + return resolveMode(getSettingWithDefault('mode', DEFAULT_MODE)) +} diff --git a/static/images/bg.webp b/static/images/bg.webp deleted file mode 100644 index 12bb165..0000000 Binary files a/static/images/bg.webp and /dev/null differ