diff --git a/bun.lock b/bun.lock index 24f51b1..2aa5ee1 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "simple-timer", "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 13958eb..f39ffa6 100644 --- a/e2e/screenshots.spec.ts +++ b/e2e/screenshots.spec.ts @@ -8,35 +8,43 @@ import { } from '@screenly/edge-apps/test/screenshots' import path from 'path' -const { screenlyJsContent } = createMockScreenlyForScreenshots( - {}, - { - duration: '60', - override_locale: 'en', - override_timezone: 'America/New_York', - display_errors: 'false', - }, -) - -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() - - // Setup mocks - 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: 'ring' }, + { prefix: 'digits-', mode: 'digits' }, +] + +for (const { prefix, mode } of VARIANTS) { + const { screenlyJsContent } = createMockScreenlyForScreenshots( + {}, + { + display_errors: 'false', + duration: '60', + mode, + override_locale: 'en', + override_timezone: 'America/New_York', + }, + ) + + 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() - await context.close() - }) + // Setup mocks + 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 a8ff1c9..85a0eaa 100644 --- a/index.html +++ b/index.html @@ -12,26 +12,19 @@ reference-height="1080" orientation="auto" > -
- -
-
-
--
-
-
-
-
00:00:00
-
--
-
-
-
-
-
+
+

+
+
+

+ +

+

+
+
+
- diff --git a/package.json b/package.json index 26cd1ac..3e21499 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,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 91429fa..306c5f3 100644 --- a/screenly.yml +++ b/screenly.yml @@ -30,6 +30,21 @@ settings: help_text: Timer duration in seconds (e.g., 60 for 1 minute, 300 for 5 minutes, 3600 for 1 hour). type: number schema_version: 1 + mode: + type: string + default_value: ring + title: Mode + optional: true + help_text: + properties: + help_text: Choose the layout for where the screen is placed. + options: + - label: Ring (date and progress ring) + value: ring + - label: Digits only (maximum readability at a distance) + value: digits + type: select + schema_version: 1 override_locale: type: string default_value: en diff --git a/screenly_qc.yml b/screenly_qc.yml index 42e6b90..792dd58 100644 --- a/screenly_qc.yml +++ b/screenly_qc.yml @@ -30,6 +30,21 @@ settings: help_text: Timer duration in seconds (e.g., 60 for 1 minute, 300 for 5 minutes, 3600 for 1 hour). type: number schema_version: 1 + mode: + type: string + default_value: ring + title: Mode + optional: true + help_text: + properties: + help_text: Choose the layout for where the screen is placed. + options: + - label: Ring (date and progress ring) + value: ring + - label: Digits only (maximum readability at a distance) + value: digits + type: select + schema_version: 1 override_locale: type: string default_value: en diff --git a/screenshots/1080x1920.webp b/screenshots/1080x1920.webp index cbfde73..58a576b 100644 Binary files a/screenshots/1080x1920.webp and b/screenshots/1080x1920.webp differ diff --git a/screenshots/1280x720.webp b/screenshots/1280x720.webp index 40cf8d7..1058020 100644 Binary files a/screenshots/1280x720.webp and b/screenshots/1280x720.webp differ diff --git a/screenshots/1920x1080.webp b/screenshots/1920x1080.webp index 24da127..906126f 100644 Binary files a/screenshots/1920x1080.webp and b/screenshots/1920x1080.webp differ diff --git a/screenshots/2160x3840.webp b/screenshots/2160x3840.webp index 04c461d..0f984c8 100644 Binary files a/screenshots/2160x3840.webp and b/screenshots/2160x3840.webp differ diff --git a/screenshots/2160x4096.webp b/screenshots/2160x4096.webp index 993cda6..7d84949 100644 Binary files a/screenshots/2160x4096.webp and b/screenshots/2160x4096.webp differ diff --git a/screenshots/3840x2160.webp b/screenshots/3840x2160.webp index 31dcace..3ae0628 100644 Binary files a/screenshots/3840x2160.webp and b/screenshots/3840x2160.webp differ diff --git a/screenshots/4096x2160.webp b/screenshots/4096x2160.webp index 329c6fa..0406e93 100644 Binary files a/screenshots/4096x2160.webp and b/screenshots/4096x2160.webp differ diff --git a/screenshots/480x800.webp b/screenshots/480x800.webp index 4f576b0..34065bf 100644 Binary files a/screenshots/480x800.webp and b/screenshots/480x800.webp differ diff --git a/screenshots/720x1280.webp b/screenshots/720x1280.webp index 83129d6..e831b4b 100644 Binary files a/screenshots/720x1280.webp and b/screenshots/720x1280.webp differ diff --git a/screenshots/800x480.webp b/screenshots/800x480.webp index 3e7560d..a8ddbd6 100644 Binary files a/screenshots/800x480.webp and b/screenshots/800x480.webp differ diff --git a/screenshots/digits-1080x1920.webp b/screenshots/digits-1080x1920.webp new file mode 100644 index 0000000..458e8af Binary files /dev/null and b/screenshots/digits-1080x1920.webp differ diff --git a/screenshots/digits-1280x720.webp b/screenshots/digits-1280x720.webp new file mode 100644 index 0000000..1308105 Binary files /dev/null and b/screenshots/digits-1280x720.webp differ diff --git a/screenshots/digits-1920x1080.webp b/screenshots/digits-1920x1080.webp new file mode 100644 index 0000000..f0e7357 Binary files /dev/null and b/screenshots/digits-1920x1080.webp differ diff --git a/screenshots/digits-2160x3840.webp b/screenshots/digits-2160x3840.webp new file mode 100644 index 0000000..cb583a1 Binary files /dev/null and b/screenshots/digits-2160x3840.webp differ diff --git a/screenshots/digits-2160x4096.webp b/screenshots/digits-2160x4096.webp new file mode 100644 index 0000000..e0d78f3 Binary files /dev/null and b/screenshots/digits-2160x4096.webp differ diff --git a/screenshots/digits-3840x2160.webp b/screenshots/digits-3840x2160.webp new file mode 100644 index 0000000..4e1bc58 Binary files /dev/null and b/screenshots/digits-3840x2160.webp differ diff --git a/screenshots/digits-4096x2160.webp b/screenshots/digits-4096x2160.webp new file mode 100644 index 0000000..00a8fa9 Binary files /dev/null and b/screenshots/digits-4096x2160.webp differ diff --git a/screenshots/digits-480x800.webp b/screenshots/digits-480x800.webp new file mode 100644 index 0000000..de2c80a Binary files /dev/null and b/screenshots/digits-480x800.webp differ diff --git a/screenshots/digits-720x1280.webp b/screenshots/digits-720x1280.webp new file mode 100644 index 0000000..7905c57 Binary files /dev/null and b/screenshots/digits-720x1280.webp differ diff --git a/screenshots/digits-800x480.webp b/screenshots/digits-800x480.webp new file mode 100644 index 0000000..a98b7c2 Binary files /dev/null and b/screenshots/digits-800x480.webp differ diff --git a/src/css/style.css b/src/css/style.css index e981c14..5c07a0d 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -1,49 +1,36 @@ -/* Import Screenly Design System */ @import '@screenly/edge-apps/styles'; -@font-face { - font-family: 'Inria Sans'; - src: url('/static/fonts/InriaSans-Bold.ttf') format('truetype'); - font-weight: 700; - font-style: normal; - font-display: swap; -} - -@font-face { - font-family: 'Inria Sans'; - src: url('/static/fonts/InriaSans-Regular.ttf') format('truetype'); - font-weight: 400; - font-style: normal; - font-display: swap; -} - -:root { - --theme-color-primary: #ac1fff; -} - -* { - box-sizing: border-box; -} - 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; - text-shadow: 0 0.125rem 0.375rem rgba(0, 0, 0, 0.25); -} - -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); + --timer-accent: var(--theme-color-primary, #972eff); + --timer-canvas: #08080d; + --timer-ink: #fff; + --timer-ink-dim: rgb(255 255 255 / 55%); + --timer-tick: rgb(255 255 255 / 22%); + --timer-accent-ink: color-mix( + in srgb, + var(--timer-accent) 65%, + var(--timer-ink) + ); + --timer-gap: 3rem; + --timer-date-size: 2.5rem; + --timer-ring-size: 56rem; + --timer-digits-size: 9rem; + --timer-total-size: 2rem; + + color: var(--timer-ink); + background-color: var(--timer-canvas); + background-image: radial-gradient( + ellipse 75% 50% at 50% 0%, + color-mix(in srgb, var(--timer-accent) 45%, transparent), + transparent + ); +} + +body.is-light-brand { + --timer-canvas: #f6f6f8; + --timer-ink: #0a0a0f; + --timer-ink-dim: rgb(10 10 15 / 58%); + --timer-tick: rgb(10 10 15 / 16%); } auto-scaler { @@ -51,122 +38,103 @@ auto-scaler { z-index: var(--z-index-content); } -#app { - width: 100%; - height: 100%; -} - -.content { - display: flex; - justify-content: center; - align-items: center; - height: calc(100% - 5rem); -} - -.timer-section { +.timer { display: flex; flex-direction: column; align-items: center; - gap: 2rem; + justify-content: center; + gap: var(--timer-gap); + width: 100%; + height: 100%; } -.date-text { - font-family: 'Inter', system-ui, sans-serif; - font-size: 2.8125rem; - font-weight: 400; - color: white; - letter-spacing: -0.1125rem; - text-shadow: 0 0.125rem 0.375rem rgba(0, 0, 0, 0.25); +.timer-date { + margin: 0; + font-size: var(--timer-date-size); + font-weight: 500; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--timer-ink-dim); } -.timer-card { +.timer-ring { position: relative; display: flex; - justify-content: center; align-items: center; - width: 41.25rem; - height: 41.25rem; - border-radius: 1.5rem; - border: 0.047rem solid rgba(255, 255, 255, 0.1); - background: - linear-gradient(90deg, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), - linear-gradient(90deg, rgba(68, 68, 68, 0.4), rgba(68, 68, 68, 0.4)), - linear-gradient( - 135deg, - rgba(255, 255, 255, 0.08) 0%, - rgba(255, 255, 255, 0.02) 100% - ); - box-shadow: - 0 0.84rem 2.52rem 0 rgba(0, 0, 0, 0.15), - 0 0.047rem 0 0 rgba(255, 255, 255, 0.15) inset, - 0 -0.047rem 0 0 rgba(255, 255, 255, 0.05) inset; -} - -.progress-ring-container { + justify-content: center; + width: var(--timer-ring-size); + height: var(--timer-ring-size); +} + +.timer-ring svg { position: absolute; inset: 0; - display: flex; - justify-content: center; - align-items: center; + width: 100%; + height: 100%; +} + +.timer-ring line { + stroke: var(--timer-tick); + stroke-width: 6; } -.progress-ring-container svg { - width: 73%; - height: 73%; +.timer-ring line.is-elapsed { + stroke: var(--timer-accent-ink); } .timer-display { - position: relative; display: flex; flex-direction: column; align-items: center; - gap: 0.5rem; - z-index: 1; + gap: 1.5rem; } .timer-digits { - font-family: 'Inria Sans', sans-serif; - font-weight: 700; - font-size: 5.8rem; + margin: 0; + font-size: var(--timer-digits-size); + font-weight: 600; line-height: 1; - letter-spacing: 0.15rem; - color: white; + letter-spacing: -0.02em; font-variant-numeric: tabular-nums; } -.timer-digits .accent { - color: var(--theme-color-primary); +.timer-seconds { + color: var(--timer-accent-ink); } .timer-total { - font-family: 'Inria Sans', sans-serif; - font-weight: 400; - font-size: 1.7rem; - color: rgba(255, 255, 255, 0.7); + margin: 0; + font-size: var(--timer-total-size); + color: var(--timer-ink-dim); } -@media (orientation: portrait) { - #app { - display: flex; - flex-direction: column; - height: 100%; - } +body.mode-digits { + --timer-digits-size: 24rem; + --timer-total-size: 3.25rem; +} - .content { - flex: 1; - height: auto; - } +body.mode-digits .timer-date { + display: none; +} - .timer-card { - width: 36rem; - height: 36rem; - } +body.mode-digits .timer-ring { + width: auto; + height: auto; +} - .timer-digits { - font-size: 5rem; +body.mode-digits .timer-display { + gap: 3rem; +} + +@media (orientation: portrait) { + body { + --timer-gap: 4.5rem; + --timer-date-size: 3rem; + --timer-ring-size: 58rem; } - .date-text { - font-size: 2.4rem; + body.mode-digits { + --timer-digits-size: 14rem; + --timer-total-size: 3rem; } } diff --git a/src/main.ts b/src/main.ts index 80f6ecc..55db6a9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,27 +1,37 @@ import './css/style.css' import { + formatLocalizedDate, + getLocale, getSettingWithDefault, getTimeZone, - getLocale, + isLightColor, + setupErrorHandling, setupTheme, signalReady, - setupErrorHandling, - formatLocalizedDate, } from '@screenly/edge-apps' +// Side-effect import: registers as a custom element import '@screenly/edge-apps/components' -import { createTimerState } from './timer' +import { getTimerMode } from './mode' import { createProgressRingSVG, updateProgressRing } from './progress-ring' +import { createTimerState } from './timer' const DATE_UPDATE_INTERVAL_MS = 60 * 1000 const TIMER_UPDATE_INTERVAL_MS = 1000 +const RING_VIEWBOX_SIZE = 400 document.addEventListener('DOMContentLoaded', async () => { setupErrorHandling() - setupTheme() + + const { primary } = setupTheme() + document.body.classList.toggle('is-light-brand', isLightColor(primary)) + + const mode = getTimerMode() + document.body.classList.add(`mode-${mode}`) const dateEl = document.querySelector('[data-date]') - const digitsEl = document.querySelector('[data-timer-digits]') + const prefixEl = document.querySelector('[data-time-prefix]') + const secondsEl = document.querySelector('[data-time-seconds]') const totalEl = document.querySelector('[data-timer-total]') const ringContainer = document.querySelector('[data-progress-ring]') @@ -31,38 +41,23 @@ document.addEventListener('DOMContentLoaded', async () => { const timezone = await getTimeZone() const locale = await getLocale() - // Update date display function updateDate() { - if (dateEl) { - const now = new Date() - dateEl.textContent = formatLocalizedDate(now, locale, { - weekday: 'long', - day: 'numeric', - month: 'long', - timeZone: timezone, - }) - } - } - - // Create progress ring - const ringSvg = createProgressRingSVG(400) - if (ringContainer) { - ringContainer.appendChild(ringSvg) + if (!dateEl) return + + dateEl.textContent = formatLocalizedDate(new Date(), locale, { + weekday: 'long', + day: 'numeric', + month: 'long', + timeZone: timezone, + }) } - let elapsedSeconds = 0 - - // Clone the timer digits template once and populate via textContent. - const digitsTemplate = document.querySelector( - '#timer-digits-template', - ) - if (digitsEl && digitsTemplate) { - const content = digitsTemplate.content.cloneNode(true) as DocumentFragment - digitsEl.textContent = '' - digitsEl.appendChild(content) + // Digits mode never shows the ring, so skip building it + const ringSvg = + mode === 'ring' ? createProgressRingSVG(RING_VIEWBOX_SIZE) : null + if (ringSvg) { + ringContainer?.appendChild(ringSvg) } - const prefixEl = digitsEl?.querySelector('[data-time-prefix]') - const secondsEl = digitsEl?.querySelector('[data-time-seconds]') function updateDisplay(state: ReturnType) { if (prefixEl) { @@ -71,14 +66,16 @@ document.addEventListener('DOMContentLoaded', async () => { if (secondsEl) { secondsEl.textContent = state.seconds } - if (totalEl) { totalEl.textContent = state.totalLabel } - - updateProgressRing(ringSvg, state.progress) + if (ringSvg) { + updateProgressRing(ringSvg, state.progress) + } } + let elapsedSeconds = 0 + updateDate() updateDisplay(createTimerState(totalDuration, elapsedSeconds)) diff --git a/src/mode.test.ts b/src/mode.test.ts new file mode 100644 index 0000000..39b91f8 --- /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('ring')).toBe('ring') + expect(resolveMode('digits')).toBe('digits') + }) + + test('normalises casing and whitespace', () => { + expect(resolveMode(' Digits ')).toBe('digits') + expect(resolveMode('RING')).toBe('ring') + }) + + test('falls back to the default for unknown values', () => { + expect(resolveMode('huge')).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..caa662b --- /dev/null +++ b/src/mode.ts @@ -0,0 +1,18 @@ +import { getSettingWithDefault } from '@screenly/edge-apps' + +export const MODES = ['ring', 'digits'] as const + +export type TimerMode = (typeof MODES)[number] + +export const DEFAULT_MODE: TimerMode = 'ring' + +// 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): TimerMode { + const normalized = value?.trim().toLowerCase() as TimerMode + return MODES.includes(normalized) ? normalized : DEFAULT_MODE +} + +export function getTimerMode(): TimerMode { + return resolveMode(getSettingWithDefault('mode', DEFAULT_MODE)) +} diff --git a/src/progress-ring.test.ts b/src/progress-ring.test.ts index 0e82b94..a3876e3 100644 --- a/src/progress-ring.test.ts +++ b/src/progress-ring.test.ts @@ -2,13 +2,15 @@ import '@screenly/edge-apps/test' import { describe, test, expect, beforeEach } from 'bun:test' import { createProgressRingSVG, updateProgressRing } from './progress-ring' +function elapsedCount(svg: SVGSVGElement): number { + return svg.querySelectorAll('line.is-elapsed').length +} + describe('createProgressRingSVG', () => { - test('creates SVG element with correct attributes', () => { + test('creates an SVG sized by its viewBox', () => { const svg = createProgressRingSVG(400) expect(svg).toBeInstanceOf(SVGSVGElement) - expect(svg.getAttribute('width')).toBe('400') - expect(svg.getAttribute('height')).toBe('400') expect(svg.getAttribute('viewBox')).toBe('0 0 400 400') }) @@ -19,15 +21,12 @@ describe('createProgressRingSVG', () => { expect(ticks.length).toBe(60) }) - test('each tick has required attributes', () => { + test('each tick has its index and geometry', () => { const svg = createProgressRingSVG(400) const ticks = svg.querySelectorAll('line[data-tick]') ticks.forEach((tick, i) => { expect(tick.getAttribute('data-tick')).toBe(String(i)) - expect(tick.getAttribute('stroke')).toBe('rgba(255, 255, 255, 0.8)') - expect(tick.getAttribute('stroke-width')).toBe('6') - expect(tick.getAttribute('stroke-linecap')).toBe('butt') expect(tick.getAttribute('x1')).not.toBeNull() expect(tick.getAttribute('y1')).not.toBeNull() expect(tick.getAttribute('x2')).not.toBeNull() @@ -38,8 +37,6 @@ describe('createProgressRingSVG', () => { test('creates SVG with different diameter', () => { const svg = createProgressRingSVG(500) - expect(svg.getAttribute('width')).toBe('500') - expect(svg.getAttribute('height')).toBe('500') expect(svg.getAttribute('viewBox')).toBe('0 0 500 500') }) }) @@ -49,82 +46,51 @@ describe('updateProgressRing', () => { beforeEach(() => { svg = createProgressRingSVG(400) - // Set up CSS custom property for theme color - document.documentElement.style.setProperty( - '--theme-color-primary', - '#ac1fff', - ) }) - test('no progress (0%) - all ticks remain default color', () => { + test('no progress (0%) leaves every tick unelapsed', () => { updateProgressRing(svg, 0) - const ticks = svg.querySelectorAll('line[data-tick]') - ticks.forEach((tick) => { - expect(tick.getAttribute('stroke')).toBe('rgba(255, 255, 255, 0.8)') - }) + expect(elapsedCount(svg)).toBe(0) }) - test('half progress (50%) - first 30 ticks colored', () => { + test('half progress (50%) marks the first 30 ticks', () => { updateProgressRing(svg, 0.5) const ticks = svg.querySelectorAll('line[data-tick]') for (let i = 0; i < 30; i++) { - expect(ticks[i].getAttribute('stroke')).toBe('#ac1fff') + expect(ticks[i].classList.contains('is-elapsed')).toBe(true) } for (let i = 30; i < 60; i++) { - expect(ticks[i].getAttribute('stroke')).toBe('rgba(255, 255, 255, 0.8)') + expect(ticks[i].classList.contains('is-elapsed')).toBe(false) } }) - test('full progress (100%) - all ticks colored', () => { - updateProgressRing(svg, 1) - const ticks = svg.querySelectorAll('line[data-tick]') - - ticks.forEach((tick) => { - expect(tick.getAttribute('stroke')).toBe('#ac1fff') - }) - }) - - test('partial progress (25%) - first 15 ticks colored', () => { + test('partial progress (25%) marks the first 15 ticks', () => { updateProgressRing(svg, 0.25) - const ticks = svg.querySelectorAll('line[data-tick]') - for (let i = 0; i < 15; i++) { - expect(ticks[i].getAttribute('stroke')).toBe('#ac1fff') - } - for (let i = 15; i < 60; i++) { - expect(ticks[i].getAttribute('stroke')).toBe('rgba(255, 255, 255, 0.8)') - } + expect(elapsedCount(svg)).toBe(15) + expect( + svg.querySelectorAll('line')[14].classList.contains('is-elapsed'), + ).toBe(true) }) - test('progress beyond 100% is handled', () => { - updateProgressRing(svg, 1.5) - const ticks = svg.querySelectorAll('line[data-tick]') + test('full progress (100%) marks every tick', () => { + updateProgressRing(svg, 1) - // Should color all 60 ticks - ticks.forEach((tick) => { - expect(tick.getAttribute('stroke')).toBe('#ac1fff') - }) + expect(elapsedCount(svg)).toBe(60) }) - test('uses custom theme color if available', () => { - document.documentElement.style.setProperty( - '--theme-color-primary', - '#ff0000', - ) - updateProgressRing(svg, 0.5) - const ticks = svg.querySelectorAll('line[data-tick]') + test('progress beyond 100% marks every tick', () => { + updateProgressRing(svg, 1.5) - expect(ticks[0].getAttribute('stroke')).toBe('#ff0000') + expect(elapsedCount(svg)).toBe(60) }) - test('falls back to default color if theme not set', () => { - document.documentElement.style.removeProperty('--theme-color-primary') - updateProgressRing(svg, 0.5) - const ticks = svg.querySelectorAll('line[data-tick]') + test('a lower progress clears previously elapsed ticks', () => { + updateProgressRing(svg, 1) + updateProgressRing(svg, 0.25) - // Should use fallback color - expect(ticks[0].getAttribute('stroke')).toBe('#ac1fff') + expect(elapsedCount(svg)).toBe(15) }) }) diff --git a/src/progress-ring.ts b/src/progress-ring.ts index 08838a2..0d4e1a8 100644 --- a/src/progress-ring.ts +++ b/src/progress-ring.ts @@ -1,20 +1,8 @@ -const REMAINING_COLOR = 'rgba(255, 255, 255, 0.8)' - -function getElapsedColor(): string { - return ( - getComputedStyle(document.documentElement) - .getPropertyValue('--theme-color-primary') - .trim() || '#ac1fff' - ) -} const SVG_NS = 'http://www.w3.org/2000/svg' const TICK_COUNT = 60 export function createProgressRingSVG(diameter: number): SVGSVGElement { - const tickCount = TICK_COUNT const svg = document.createElementNS(SVG_NS, 'svg') - svg.setAttribute('width', `${diameter}`) - svg.setAttribute('height', `${diameter}`) svg.setAttribute('viewBox', `0 0 ${diameter} ${diameter}`) const cx = diameter / 2 @@ -23,8 +11,8 @@ export function createProgressRingSVG(diameter: number): SVGSVGElement { const tickLength = diameter * 0.04 const innerRadius = outerRadius - tickLength - for (let i = 0; i < tickCount; i++) { - const angle = (i / tickCount) * 2 * Math.PI - Math.PI / 2 + for (let i = 0; i < TICK_COUNT; i++) { + const angle = (i / TICK_COUNT) * 2 * Math.PI - Math.PI / 2 const x1 = cx + innerRadius * Math.cos(angle) const y1 = cy + innerRadius * Math.sin(angle) const x2 = cx + outerRadius * Math.cos(angle) @@ -35,9 +23,6 @@ export function createProgressRingSVG(diameter: number): SVGSVGElement { line.setAttribute('y1', `${y1}`) line.setAttribute('x2', `${x2}`) line.setAttribute('y2', `${y2}`) - line.setAttribute('stroke', REMAINING_COLOR) - line.setAttribute('stroke-width', '6') - line.setAttribute('stroke-linecap', 'butt') line.setAttribute('data-tick', `${i}`) svg.appendChild(line) } @@ -47,13 +32,9 @@ export function createProgressRingSVG(diameter: number): SVGSVGElement { export function updateProgressRing(svg: SVGSVGElement, progress: number): void { const ticks = svg.querySelectorAll('line[data-tick]') - const tickCount = ticks.length - const elapsedTicks = Math.floor(progress * tickCount) + const elapsedTicks = Math.floor(progress * ticks.length) ticks.forEach((tick, i) => { - tick.setAttribute( - 'stroke', - i < elapsedTicks ? getElapsedColor() : REMAINING_COLOR, - ) + tick.classList.toggle('is-elapsed', i < elapsedTicks) }) } diff --git a/static/fonts/InriaSans-Bold.ttf b/static/fonts/InriaSans-Bold.ttf deleted file mode 100644 index 06032d5..0000000 Binary files a/static/fonts/InriaSans-Bold.ttf and /dev/null differ diff --git a/static/fonts/InriaSans-Regular.ttf b/static/fonts/InriaSans-Regular.ttf deleted file mode 100644 index 387d683..0000000 Binary files a/static/fonts/InriaSans-Regular.ttf and /dev/null differ 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