diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index e4316a5..d694c2a 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -3,9 +3,11 @@ name: Checks on: push: branches: + - development - main pull_request: branches: + - development - main jobs: diff --git a/bun.lock b/bun.lock index 8669490..ca9469e 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,7 @@ }, "devDependencies": { "@playwright/test": "^1.58.0", - "@screenly/edge-apps": "^1.2.1", + "@screenly/edge-apps": "^1.5.0", "@types/bun": "^1.3.13", "@types/jsdom": "^28.0.1", "bun-types": "^1.3.13", @@ -47,7 +47,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=="], @@ -123,53 +123,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=="], @@ -419,7 +423,7 @@ "@rowanmanning/feed-parser": ["@rowanmanning/feed-parser@2.1.3", "", { "dependencies": { "fast-xml-parser": "^5.5.9", "html-entities": "^2.3.3" } }, "sha512-sfr85UZqCNGwPwJ8pBqnZq/vO4wrC/UEfNuCnJAtNSPPQCK7w1sfH6PAzwfNpaAK3erafFFm6m+6TKyCSFw6Bw=="], - "@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=="], @@ -887,11 +891,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=="], @@ -991,9 +995,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=="], @@ -1015,6 +1029,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.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], "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 4af214d..903a405 100644 --- a/e2e/screenshots.spec.ts +++ b/e2e/screenshots.spec.ts @@ -9,9 +9,16 @@ import { import path from 'path' const RSS_URL = 'http://feeds.bbci.co.uk/news/rss.xml' +const MOCK_IMAGE_URL_PATTERN = 'https://mock-images.example/**' + +const MOCK_IMAGE_SVG = ` + + + +` const MOCK_RSS_XML = ` - + BBC News https://www.bbc.co.uk/news @@ -20,16 +27,19 @@ const MOCK_RSS_XML = ` Global leaders gather for climate summit in Geneva World leaders are meeting in Geneva to discuss urgent measures to address climate change, with new emissions targets expected to be announced. Mon, 08 Dec 2025 09:00:00 GMT + Tech giants face new antitrust regulations across Europe The European Commission has unveiled a sweeping set of antitrust measures targeting major technology companies operating within the EU. Mon, 08 Dec 2025 08:30:00 GMT + Scientists discover new species deep in the Amazon rainforest A team of researchers has identified over a dozen previously unknown species during an expedition into a remote region of the Amazon. Mon, 08 Dec 2025 07:45:00 GMT + Global markets rally as inflation data shows signs of easing @@ -40,6 +50,7 @@ const MOCK_RSS_XML = ` Historic peace agreement signed in the Middle East Diplomats have announced a landmark peace agreement following months of negotiations, raising hopes for long-term stability in the region. Mon, 08 Dec 2025 06:30:00 GMT + Space agency reveals plans for crewed Mars mission by 2035 @@ -49,49 +60,65 @@ const MOCK_RSS_XML = ` ` -const { screenlyJsContent } = createMockScreenlyForScreenshots( - { - coordinates: [51.5074, -0.1278], - location: 'London, UK', - }, - { - bypass_cors: 'false', - cache_interval: '1800', - display_errors: 'false', - enable_analytics: 'false', - override_locale: 'en', - override_timezone: 'Europe/London', - rss_title: 'BBC News', - rss_url: RSS_URL, - }, -) +const VARIANTS = [ + { prefix: '', mode: 'headlines' }, + { prefix: 'list-', mode: 'list' }, +] + +for (const { prefix, mode } of VARIANTS) { + const { screenlyJsContent } = createMockScreenlyForScreenshots( + { + coordinates: [51.5074, -0.1278], + location: 'London, UK', + }, + { + bypass_cors: 'false', + cache_interval: '1800', + display_errors: 'false', + enable_analytics: 'false', + mode, + override_locale: 'en', + override_timezone: 'Europe/London', + rss_title: 'BBC News', + rss_url: RSS_URL, + }, + ) -for (const { width, height } of RESOLUTIONS) { - test(`screenshot ${width}x${height}`, async ({ browser }) => { - const screenshotsDir = getScreenshotsDir() + 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 } }) - const page = await context.newPage() + const context = await browser.newContext({ viewport: { width, height } }) + const page = await context.newPage() - await setupClockMock(page) - await setupScreenlyJsMock(page, screenlyJsContent) + await setupClockMock(page) + await setupScreenlyJsMock(page, screenlyJsContent) - await page.route(RSS_URL, async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/rss+xml', - body: MOCK_RSS_XML, + await page.route(RSS_URL, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/rss+xml', + body: MOCK_RSS_XML, + }) + }) + + await page.route(MOCK_IMAGE_URL_PATTERN, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'image/svg+xml', + body: MOCK_IMAGE_SVG, + }) }) - }) - await page.goto('/') - await page.waitForLoadState('networkidle') + await page.goto('/') + await page.waitForLoadState('networkidle') - await page.screenshot({ - path: path.join(screenshotsDir, `${width}x${height}.png`), - fullPage: false, - }) + await page.screenshot({ + path: path.join(screenshotsDir, `${prefix}${width}x${height}.png`), + fullPage: false, + }) - await context.close() - }) + await context.close() + }) + } } diff --git a/index.html b/index.html index 71a55be..5bf8868 100644 --- a/index.html +++ b/index.html @@ -12,28 +12,25 @@ reference-height="1080" orientation="auto" > -
- - - - -

- -
- - + + diff --git a/package.json b/package.json index a5a4168..55a62a4 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "devDependencies": { "@playwright/test": "^1.58.0", - "@screenly/edge-apps": "^1.2.1", + "@screenly/edge-apps": "^1.5.0", "@types/bun": "^1.3.13", "@types/jsdom": "^28.0.1", "bun-types": "^1.3.13", diff --git a/screenly.yml b/screenly.yml index 19a81ea..6c96ad8 100644 --- a/screenly.yml +++ b/screenly.yml @@ -41,6 +41,21 @@ settings: optional: true help_text: Whether to enable analytics or not is_global: true + mode: + type: string + default_value: headlines + title: Mode + optional: true + help_text: + properties: + help_text: Choose the layout for where the screen is placed. + options: + - label: Headlines (lead story and list) + value: headlines + - label: List (headlines only, maximum readability) + value: list + type: select + schema_version: 1 override_locale: type: string default_value: en diff --git a/screenly_qc.yml b/screenly_qc.yml index c94e8e3..81b2b30 100644 --- a/screenly_qc.yml +++ b/screenly_qc.yml @@ -41,6 +41,21 @@ settings: optional: true help_text: Whether to enable analytics or not is_global: true + mode: + type: string + default_value: headlines + title: Mode + optional: true + help_text: + properties: + help_text: Choose the layout for where the screen is placed. + options: + - label: Headlines (lead story and list) + value: headlines + - label: List (headlines only, maximum readability) + value: list + type: select + schema_version: 1 override_locale: type: string default_value: en diff --git a/screenshots/1080x1920.webp b/screenshots/1080x1920.webp index 416d6ef..32ac344 100644 Binary files a/screenshots/1080x1920.webp and b/screenshots/1080x1920.webp differ diff --git a/screenshots/1280x720.webp b/screenshots/1280x720.webp index bbc253b..d634d70 100644 Binary files a/screenshots/1280x720.webp and b/screenshots/1280x720.webp differ diff --git a/screenshots/1920x1080.webp b/screenshots/1920x1080.webp index 88bc8ed..3a8d7b2 100644 Binary files a/screenshots/1920x1080.webp and b/screenshots/1920x1080.webp differ diff --git a/screenshots/2160x3840.webp b/screenshots/2160x3840.webp index 96a69e5..621bb78 100644 Binary files a/screenshots/2160x3840.webp and b/screenshots/2160x3840.webp differ diff --git a/screenshots/2160x4096.webp b/screenshots/2160x4096.webp index 1a91df1..10a6301 100644 Binary files a/screenshots/2160x4096.webp and b/screenshots/2160x4096.webp differ diff --git a/screenshots/3840x2160.webp b/screenshots/3840x2160.webp index e86b3e5..65f3568 100644 Binary files a/screenshots/3840x2160.webp and b/screenshots/3840x2160.webp differ diff --git a/screenshots/4096x2160.webp b/screenshots/4096x2160.webp index 112a211..6d330ac 100644 Binary files a/screenshots/4096x2160.webp and b/screenshots/4096x2160.webp differ diff --git a/screenshots/480x800.webp b/screenshots/480x800.webp index 8adedba..685b09e 100644 Binary files a/screenshots/480x800.webp and b/screenshots/480x800.webp differ diff --git a/screenshots/720x1280.webp b/screenshots/720x1280.webp index 250c86b..262ab2b 100644 Binary files a/screenshots/720x1280.webp and b/screenshots/720x1280.webp differ diff --git a/screenshots/800x480.webp b/screenshots/800x480.webp index 36068d4..5ec1678 100644 Binary files a/screenshots/800x480.webp and b/screenshots/800x480.webp differ diff --git a/screenshots/list-1080x1920.webp b/screenshots/list-1080x1920.webp new file mode 100644 index 0000000..390cb17 Binary files /dev/null and b/screenshots/list-1080x1920.webp differ diff --git a/screenshots/list-1280x720.webp b/screenshots/list-1280x720.webp new file mode 100644 index 0000000..21ee2bc Binary files /dev/null and b/screenshots/list-1280x720.webp differ diff --git a/screenshots/list-1920x1080.webp b/screenshots/list-1920x1080.webp new file mode 100644 index 0000000..6c9a8bd Binary files /dev/null and b/screenshots/list-1920x1080.webp differ diff --git a/screenshots/list-2160x3840.webp b/screenshots/list-2160x3840.webp new file mode 100644 index 0000000..3be8a62 Binary files /dev/null and b/screenshots/list-2160x3840.webp differ diff --git a/screenshots/list-2160x4096.webp b/screenshots/list-2160x4096.webp new file mode 100644 index 0000000..3e2f449 Binary files /dev/null and b/screenshots/list-2160x4096.webp differ diff --git a/screenshots/list-3840x2160.webp b/screenshots/list-3840x2160.webp new file mode 100644 index 0000000..ddff69e Binary files /dev/null and b/screenshots/list-3840x2160.webp differ diff --git a/screenshots/list-4096x2160.webp b/screenshots/list-4096x2160.webp new file mode 100644 index 0000000..1b3d230 Binary files /dev/null and b/screenshots/list-4096x2160.webp differ diff --git a/screenshots/list-480x800.webp b/screenshots/list-480x800.webp new file mode 100644 index 0000000..e322566 Binary files /dev/null and b/screenshots/list-480x800.webp differ diff --git a/screenshots/list-720x1280.webp b/screenshots/list-720x1280.webp new file mode 100644 index 0000000..503773a Binary files /dev/null and b/screenshots/list-720x1280.webp differ diff --git a/screenshots/list-800x480.webp b/screenshots/list-800x480.webp new file mode 100644 index 0000000..d3d72f9 Binary files /dev/null and b/screenshots/list-800x480.webp differ diff --git a/src/css/style.css b/src/css/style.css index e8e83ff..36f8140 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -1,49 +1,34 @@ @import '@screenly/edge-apps/styles'; -:root { - --card-bg: - linear-gradient(90deg, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.4) 100%), - linear-gradient( - 142deg, - rgba(255, 255, 255, 0.08) 0%, - rgba(255, 255, 255, 0.02) 100% - ); - --card-border: rgba(255, 255, 255, 0.1); - --card-radius: 1.667rem; - --card-padding: 1.5rem; - --card-gap: 1.25rem; - --grid-gap: 1.25rem; - --text-primary: #ffffff; - --text-muted: rgba(255, 255, 255, 0.8); -} - -* { - box-sizing: border-box; -} - -#feed-grid[hidden], -#feed-error[hidden] { - display: none; -} - body { - margin: 0; - padding: 0; - overflow: hidden; - color: var(--text-primary); - font-family: 'Inter', system-ui, sans-serif; - background: url('/static/images/bg.webp') no-repeat center center; - background-size: cover; + --news-accent: var(--theme-color-primary, #972eff); + --news-canvas: #08080d; + --news-ink: #fff; + --news-ink-dim: rgb(255 255 255 / 55%); + --news-rule: rgb(255 255 255 / 14%); + --news-accent-ink: color-mix( + in srgb, + var(--news-accent) 65%, + var(--news-ink) + ); + --news-hero-size: 6rem; + --news-item-size: 2.25rem; + --news-item-date-size: 1.4rem; + + color: var(--news-ink); + background-color: var(--news-canvas); + background-image: radial-gradient( + ellipse 75% 50% at 50% 0%, + color-mix(in srgb, var(--news-accent) 40%, transparent), + transparent + ); } -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(3px); +body.is-light-brand { + --news-canvas: #f6f6f8; + --news-ink: #0a0a0f; + --news-ink-dim: rgb(10 10 15 / 58%); + --news-rule: rgb(10 10 15 / 14%); } auto-scaler { @@ -51,171 +36,278 @@ auto-scaler { z-index: var(--z-index-content); } -#app { +.news { display: flex; flex-direction: column; + gap: 3rem; width: 100%; height: 100%; + padding: 3rem 3.5rem; } -.header { - border-radius: 1.042rem; - padding-right: 1.25rem; - background: rgba(0, 0, 0, 0.2); - box-shadow: - inset 0 0.052rem 0 rgba(255, 255, 255, 0.15), - inset 0 -0.052rem 0 rgba(255, 255, 255, 0.05); +.news-body[hidden], +.news-error[hidden] { + display: none; +} + +.news-header { + display: flex; + align-items: center; + gap: 1.75rem; } -.feed-date { +.news-logo { + max-width: 12rem; + max-height: 3.5rem; + object-fit: contain; +} + +.news-logo.is-hidden { + display: none; +} + +.news-source { margin: 0; - padding: 5.25rem 10rem 0; - font-size: 2.344rem; - font-weight: 400; - line-height: 3.073rem; - color: var(--text-primary); - text-align: center; - text-shadow: 0 0.104rem 0.313rem rgba(0, 0, 0, 0.25); - white-space: nowrap; + font-size: 2rem; + font-weight: 600; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.news-date { + margin: 0 0 0 auto; + font-size: 2rem; + color: var(--news-ink-dim); + font-variant-numeric: tabular-nums; } -.feed-grid { +.news-body { flex: 1; - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: var(--grid-gap); - padding: 3.75rem 10rem 7.5rem; - overflow: hidden; + min-height: 0; + display: flex; + gap: 4.5rem; } -.feed-card { +.news-hero { + flex: 1 1 55%; + min-width: 0; display: flex; flex-direction: column; - gap: var(--card-gap); - padding: var(--card-padding); - background: var(--card-bg); - border: 0.75px solid var(--card-border); - border-radius: var(--card-radius); - backdrop-filter: blur(10px); + justify-content: center; + gap: 2.5rem; + padding-left: 4rem; + border-left: 0.5rem solid var(--news-accent-ink); +} + +.news-hero-image { + width: 100%; + height: 15rem; + object-fit: cover; + border-radius: 1.25rem; + border: 0.0625rem solid var(--news-rule); +} + +.news-hero-image.is-hidden, +.news-item-image.is-hidden { + display: none; +} + +.news-hero-date { + margin: 0; + font-size: 1.6rem; + font-weight: 600; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--news-accent-ink); +} + +.news-hero-title { + margin: 0; + font-size: var(--news-hero-size); + font-weight: 600; + line-height: 1.05; + letter-spacing: -0.02em; + overflow-wrap: anywhere; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; overflow: hidden; - box-shadow: - inset 0 0.75px 0 rgba(255, 255, 255, 0.15), - inset 0 -0.75px 0 rgba(255, 255, 255, 0.05); } -.feed-card-meta { - display: flex; - flex-direction: column; - gap: 0.375rem; +.news-hero-title.is-long { + font-size: 4.75rem; + -webkit-line-clamp: 4; +} + +.news-hero-title.is-very-long { + font-size: 3.75rem; + -webkit-line-clamp: 5; } -.feed-card-title { +.news-hero-excerpt { margin: 0; - font-size: 1.458rem; - font-weight: 700; - line-height: 1.929rem; - color: var(--text-primary); + max-width: 56ch; + font-size: 2.1rem; + line-height: 1.5; + color: var(--news-ink-dim); display: -webkit-box; -webkit-box-orient: vertical; - -webkit-line-clamp: 2; + -webkit-line-clamp: 4; overflow: hidden; } -.feed-card-date { - margin-top: 0.5rem; - font-size: 1.042rem; - font-weight: 400; - line-height: 1.563rem; - color: var(--text-muted); - white-space: nowrap; +.news-list { + flex: 1 1 45%; + min-width: 0; + margin: 0; + padding: 0; + list-style: none; + display: flex; + flex-direction: column; + justify-content: space-evenly; + overflow: hidden; } -.feed-card-source { - font-size: 0.833rem; - font-weight: 600; - line-height: 1.25rem; - color: var(--text-muted); - letter-spacing: 0.08em; - text-transform: uppercase; +.news-item { + display: flex; + align-items: center; + gap: 2rem; + padding: 1.25rem 0; + border-top: 0.0625rem solid var(--news-rule); } -.feed-card-excerpt { +.news-item-text { flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.news-item-image { + flex: 0 0 auto; + width: 9rem; + aspect-ratio: 16 / 9; + object-fit: cover; + border-radius: 0.75rem; + border: 0.0625rem solid var(--news-rule); +} + +.news-item:first-child { + border-top: 0; +} + +.news-item-title { margin: 0; - font-size: 1.042rem; - font-weight: 500; - line-height: 1.563rem; - color: var(--text-primary); + font-size: var(--news-item-size); + font-weight: 600; + line-height: 1.25; + overflow-wrap: anywhere; display: -webkit-box; -webkit-box-orient: vertical; - -webkit-line-clamp: 4; + -webkit-line-clamp: 2; overflow: hidden; } -.feed-error { +.news-item-date { + margin: 0; + font-size: var(--news-item-date-size); + color: var(--news-ink-dim); + font-variant-numeric: tabular-nums; +} + +.news-error { flex: 1; display: flex; align-items: center; justify-content: center; - padding-bottom: 10%; - font-size: 1.5rem; - line-height: 3rem; - color: var(--text-muted); + margin: 0; + font-size: 3.5rem; + line-height: 1.4; text-align: center; + color: var(--news-ink-dim); +} + +body.mode-list { + --news-item-size: 3.4rem; + --news-item-date-size: 1.75rem; +} + +body.mode-list .news-hero { + display: none; +} + +body.mode-list .news-item { + padding: 2rem 0; +} + +body.mode-list .news-item-text { + flex-direction: row; + align-items: baseline; + justify-content: space-between; + gap: 3rem; +} + +body.mode-list .news-item-title { + flex: 1; + display: block; + white-space: nowrap; + text-overflow: ellipsis; +} + +body.mode-list .news-item-date { + white-space: nowrap; } @media (orientation: portrait) { - .header { - padding: 0.938rem 1.25rem; - border-radius: 1.25rem; - font-size: 1.5rem; + body { + --news-hero-size: 5rem; + --news-item-size: 2.8rem; + --news-item-date-size: 1.7rem; } - .feed-date { - padding: 10rem 2.5rem 0; - font-size: 5.625rem; - line-height: 7.375rem; - white-space: normal; + .news { + padding: 3.5rem 3rem; + } + + .news-body { + flex-direction: column; + gap: 3.5rem; + } + + .news-hero { + flex: 0 0 auto; + gap: 2rem; + padding-left: 3rem; } - .feed-grid { - flex: unset; - margin-top: auto; - grid-template-columns: 1fr; - grid-auto-rows: 1fr; - align-content: start; - padding: 3.75rem 2.5rem 7.5rem; + .news-hero-image { + height: 12rem; } - .feed-card { - gap: 2.5rem; - padding: 3.25rem; + .news-hero-excerpt { + -webkit-line-clamp: 3; } - .feed-card:nth-child(n + 4) { + body.mode-headlines .news-item:nth-child(n + 5) { display: none; } - .feed-card-title { - font-size: 2rem; - line-height: 2.625rem; - -webkit-line-clamp: 2; + body.mode-list { + --news-item-size: 3rem; } - .feed-card-date { - margin-top: 0.75rem; - font-size: 1.5rem; - line-height: 2rem; + body.mode-list .news-item { + padding: 1.75rem 0; } - .feed-card-source { - font-size: 1.125rem; - line-height: 1.5rem; + body.mode-list .news-item-text { + flex-direction: column; + gap: 0.75rem; } - .feed-card-excerpt { - font-size: 1.5rem; - line-height: 2rem; - -webkit-line-clamp: 3; + body.mode-list .news-item-title { + display: -webkit-box; + white-space: normal; } } diff --git a/src/main.ts b/src/main.ts index 364d42b..9841ef7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,6 @@ import './css/style.css' + +// Side-effect import: registers as a custom element import '@screenly/edge-apps/components' import { parseFeed } from '@rowanmanning/feed-parser' import { @@ -7,24 +9,56 @@ import { getLocale, getSettingWithDefault, getTimeZone, + isLightColor, + setupBrandingLogo, setupErrorHandling, + setupTheme, signalReady, } from '@screenly/edge-apps' -import { loadCache, saveCache, stripHtml, type RssEntry } from './utils' +import { getNewsMode, type NewsMode } from './mode' +import { + getTitleSizeClass, + loadCache, + saveCache, + stripHtml, + type RssEntry, +} from './utils' const MAX_ENTRIES = 6 +const DATE_UPDATE_INTERVAL_MS = 60 * 1000 + +interface FeedConfig { + rssUrl: string + rssTitle: string + locale: string + timezone: string +} + +function queryElements() { + return { + source: document.querySelector('[data-source]'), + date: document.querySelector('[data-date]'), + body: document.querySelector('[data-body]'), + heroDate: document.querySelector('[data-hero-date]'), + heroTitle: document.querySelector('[data-hero-title]'), + heroExcerpt: document.querySelector('[data-hero-excerpt]'), + heroImage: document.querySelector('[data-hero-image]'), + list: document.querySelector('[data-list]'), + error: document.querySelector('[data-error]'), + logo: document.querySelector('[data-logo]'), + } +} + +type NewsElements = ReturnType -async function fetchFeed( - rssUrl: string, - locale: string, - timezone: string, - source: string, -): Promise { +async function fetchFeed(config: FeedConfig): Promise { const bypassCors = getSettingWithDefault('bypass_cors', 'true') === 'true' - const isAbsolute = /^https?:/.test(rssUrl) + const isAbsolute = /^https?:/.test(config.rssUrl) const url = - bypassCors && isAbsolute ? `${getCorsProxyUrl()}/${rssUrl}` : rssUrl + bypassCors && isAbsolute + ? `${getCorsProxyUrl()}/${config.rssUrl}` + : config.rssUrl const response = await fetch(url) if (!response.ok) { @@ -37,11 +71,12 @@ async function fetchFeed( const rawContent = item.content ?? item.description ?? '' return { title: item.title ?? '', - source, + source: config.rssTitle, content: rawContent.includes('<') ? stripHtml(rawContent) : rawContent, + imageUrl: item.image?.url ?? '', formattedDate: item.published - ? formatLocalizedDate(item.published, locale, { - timeZone: timezone, + ? formatLocalizedDate(item.published, config.locale, { + timeZone: config.timezone, weekday: 'short', month: 'short', day: 'numeric', @@ -54,84 +89,162 @@ async function fetchFeed( }) } -function renderCards(entries: RssEntry[]) { - const grid = document.getElementById('feed-grid')! - const template = document.getElementById( - 'feed-card-template', - ) as HTMLTemplateElement - - grid.innerHTML = '' - - for (const entry of entries) { - const clone = template.content.cloneNode(true) as DocumentFragment - clone.querySelector('.feed-card-title')!.textContent = - entry.title - clone.querySelector('.feed-card-date')!.textContent = - entry.formattedDate - clone.querySelector('.feed-card-source')!.textContent = - entry.source - clone.querySelector('.feed-card-excerpt')!.textContent = - entry.content - grid.appendChild(clone) +function createRow(entry: RssEntry, withImage: boolean): HTMLLIElement { + const item = document.createElement('li') + item.className = 'news-item' + + const text = document.createElement('div') + text.className = 'news-item-text' + + const title = document.createElement('p') + title.className = 'news-item-title' + title.textContent = entry.title + + const date = document.createElement('p') + date.className = 'news-item-date' + date.textContent = entry.formattedDate + + text.append(title, date) + item.appendChild(text) + + if (withImage && entry.imageUrl) { + const image = document.createElement('img') + image.className = 'news-item-image' + image.alt = '' + image.onerror = () => image.classList.add('is-hidden') + image.src = entry.imageUrl + item.appendChild(image) + } + + return item +} + +function renderHero(els: NewsElements, entry: RssEntry) { + if (els.heroImage) { + els.heroImage.classList.toggle('is-hidden', !entry.imageUrl) + if (entry.imageUrl) { + els.heroImage.src = entry.imageUrl + } + } + if (els.heroDate) { + els.heroDate.textContent = entry.formattedDate + } + if (els.heroTitle) { + els.heroTitle.className = [ + 'news-hero-title', + getTitleSizeClass(entry.title), + ] + .filter(Boolean) + .join(' ') + els.heroTitle.textContent = entry.title + } + if (els.heroExcerpt) { + els.heroExcerpt.textContent = entry.content } } -function showGrid() { - document.getElementById('feed-grid')!.hidden = false - document.getElementById('feed-error')!.hidden = true +function render(els: NewsElements, mode: NewsMode, entries: RssEntry[]) { + const listEntries = mode === 'headlines' ? entries.slice(1) : entries + // The list mode is the text-first layout, so it skips thumbnails + els.list?.replaceChildren( + ...listEntries.map((entry) => createRow(entry, mode === 'headlines')), + ) + + // The list mode never shows the lead story, so skip rendering it + if (mode !== 'headlines') return + renderHero(els, entries[0]) } -function showError() { - document.getElementById('feed-grid')!.hidden = true - document.getElementById('feed-error')!.hidden = false +function showBody(els: NewsElements) { + if (els.body) els.body.hidden = false + if (els.error) els.error.hidden = true +} + +function showError(els: NewsElements) { + if (els.body) els.body.hidden = true + if (els.error) els.error.hidden = false +} + +async function loadAndRender( + els: NewsElements, + mode: NewsMode, + config: FeedConfig, +) { + try { + const entries = await fetchFeed(config) + if (entries.length === 0) { + showError(els) + return + } + saveCache(entries) + render(els, mode, entries) + showBody(els) + } catch (err) { + console.error('RSS fetch failed:', err) + const cached = loadCache() + if (cached.length === 0) { + showError(els) + return + } + render(els, mode, cached) + showBody(els) + } +} + +function updateDate(els: NewsElements, config: FeedConfig) { + if (!els.date) return + + els.date.textContent = formatLocalizedDate(new Date(), config.locale, { + day: 'numeric', + month: 'long', + year: 'numeric', + timeZone: config.timezone, + }) } document.addEventListener('DOMContentLoaded', async () => { setupErrorHandling() - const rssUrl = getSettingWithDefault( - 'rss_url', - 'http://feeds.bbci.co.uk/news/rss.xml', - ) - const rssTitle = getSettingWithDefault('rss_title', 'RSS Feed') - const cacheInterval = - getSettingWithDefault('cache_interval', 1800) * 1000 + const { primary } = setupTheme() + document.body.classList.toggle('is-light-brand', isLightColor(primary)) - const loadAndRender = async () => { - try { - const entries = await fetchFeed(rssUrl, locale, timezone, rssTitle) - saveCache(entries) - renderCards(entries) - showGrid() - } catch (err) { - console.error('RSS fetch failed:', err) - const cached = loadCache() - if (cached.length === 0) { - showError() - return - } - renderCards(cached) - showGrid() - } + const mode = getNewsMode() + document.body.classList.add(`mode-${mode}`) + + const els = queryElements() + + const heroImage = els.heroImage + if (heroImage) { + heroImage.onerror = () => heroImage.classList.add('is-hidden') } - const updateDate = () => { - const dateEl = document.getElementById('feed-date')! - dateEl.textContent = formatLocalizedDate(new Date(), locale, { - day: 'numeric', - month: 'long', - year: 'numeric', - timeZone: timezone, - }) + const logo = els.logo + if (logo) { + logo.onerror = () => logo.classList.add('is-hidden') + logo.src = await setupBrandingLogo() } - const locale = await getLocale() - const timezone = await getTimeZone() + const rssTitle = getSettingWithDefault('rss_title', 'RSS Feed') + if (els.source) { + els.source.textContent = rssTitle + } + + const config: FeedConfig = { + rssUrl: getSettingWithDefault( + 'rss_url', + 'http://feeds.bbci.co.uk/news/rss.xml', + ), + rssTitle, + locale: await getLocale(), + timezone: await getTimeZone(), + } + const cacheInterval = + getSettingWithDefault('cache_interval', 1800) * 1000 - updateDate() - setInterval(updateDate, 60000) + updateDate(els, config) + setInterval(() => updateDate(els, config), DATE_UPDATE_INTERVAL_MS) - await loadAndRender() + await loadAndRender(els, mode, config) signalReady() - setInterval(loadAndRender, cacheInterval) + setInterval(() => loadAndRender(els, mode, config), cacheInterval) }) diff --git a/src/mode.test.ts b/src/mode.test.ts new file mode 100644 index 0000000..6c3947f --- /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('headlines')).toBe('headlines') + expect(resolveMode('list')).toBe('list') + }) + + test('normalises casing and whitespace', () => { + expect(resolveMode(' List ')).toBe('list') + expect(resolveMode('HEADLINES')).toBe('headlines') + }) + + test('falls back to the default for unknown values', () => { + expect(resolveMode('grid')).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..6d63327 --- /dev/null +++ b/src/mode.ts @@ -0,0 +1,18 @@ +import { getSettingWithDefault } from '@screenly/edge-apps' + +export const MODES = ['headlines', 'list'] as const + +export type NewsMode = (typeof MODES)[number] + +export const DEFAULT_MODE: NewsMode = 'headlines' + +// 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): NewsMode { + const normalized = value?.trim().toLowerCase() as NewsMode + return MODES.includes(normalized) ? normalized : DEFAULT_MODE +} + +export function getNewsMode(): NewsMode { + return resolveMode(getSettingWithDefault('mode', DEFAULT_MODE)) +} diff --git a/src/utils.test.ts b/src/utils.test.ts index a295052..4d60eaf 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect, beforeEach } from 'bun:test' -import { stripHtml, loadCache, saveCache } from './utils' +import { getTitleSizeClass, stripHtml, loadCache, saveCache } from './utils' import type { RssEntry } from './utils' const localStorageMock = (() => { @@ -23,12 +23,14 @@ const ENTRIES: RssEntry[] = [ source: 'BBC News', content: 'Content 1', formattedDate: 'Mon, Dec 8, 2025', + imageUrl: 'https://example.com/image-1.jpg', }, { title: 'Title 2', source: 'BBC News', content: 'Content 2', formattedDate: 'Mon, Dec 8, 2025', + imageUrl: '', }, ] @@ -75,3 +77,22 @@ describe('loadCache / saveCache', () => { expect(loadCache()).toEqual([]) }) }) + +describe('getTitleSizeClass', () => { + test('returns no class for a short title', () => { + expect(getTitleSizeClass('Markets rally')).toBe('') + }) + + test('returns is-long past 70 characters', () => { + expect(getTitleSizeClass('a'.repeat(71))).toBe('is-long') + }) + + test('returns is-very-long past 130 characters', () => { + expect(getTitleSizeClass('a'.repeat(131))).toBe('is-very-long') + }) + + test('treats the thresholds as exclusive', () => { + expect(getTitleSizeClass('a'.repeat(70))).toBe('') + expect(getTitleSizeClass('a'.repeat(130))).toBe('is-long') + }) +}) diff --git a/src/utils.ts b/src/utils.ts index d342f18..6293e9e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,6 +4,7 @@ export interface RssEntry { title: string content: string formattedDate: string + imageUrl: string source: string } @@ -12,6 +13,17 @@ interface AppCache { timestamp: number } +const LONG_TITLE_CHARS = 70 +const VERY_LONG_TITLE_CHARS = 130 + +// News titles vary wildly in length; a long one steps down in size instead +// of wrapping the lead story off the canvas +export function getTitleSizeClass(title: string): string { + if (title.length > VERY_LONG_TITLE_CHARS) return 'is-very-long' + if (title.length > LONG_TITLE_CHARS) return 'is-long' + return '' +} + export function stripHtml(html: string): string { return html .replace(/<[^>]*>/g, '') diff --git a/static/images/bg.webp b/static/images/bg.webp deleted file mode 100644 index 202cb6f..0000000 Binary files a/static/images/bg.webp and /dev/null differ