Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ settings apps). There is **no server**; the clock ticks entirely in the browser.

Like Opening Hours, this is a **settings** app: the target isn't baked in, it
arrives in the launch URL's query string (`?title=…&target=…&tz=…&message=…`).
**Direction is automatic** a future target counts down, a past one counts up;
**Direction is automatic**: a future target counts down, a past one counts up;
there is no mode setting. Single self-ticking page (a `setTimeout` loop aligned
to the wall-clock second); the player reloads on its own schedule.

## Stack & conventions

- **Bun** for everything (package manager, bundler, test runner). Use `bun` /
`bunx` never npm/npx.
`bunx`, never npm/npx.
- **TypeScript**, strict. All browser JS is authored as `.ts` and bundled by Bun.
- **Tailwind CSS v4**, CSS-first: tokens live in `@theme` in
`assets/static/styles/tailwind.css`; compiled by `@tailwindcss/cli` at build.
Expand All @@ -32,7 +32,7 @@ to the wall-clock second); the player reloads on its own schedule.
bun install # deps; vendored fonts come from @fontsource via sync-fonts
bun run dev # build + serve dist/ locally
bun run build # assemble dist/ (see below)
bun test # bun:test date math + manifest validation
bun test # bun:test for date math + manifest validation
bun run typecheck # tsc --noEmit
bun run lint # biome lint --error-on-warnings
```
Expand All @@ -42,17 +42,17 @@ bun run lint # biome lint --error-on-warnings
Web root is served from the site root (custom domain), so assets are referenced
absolutely as `/static/...`.

- `index.html` the page shell. Ships a worked example inline (New Year) so the
- `index.html`: the page shell. Ships a worked example inline (New Year) so the
screen is never blank pre-JS or in the store preview. Asset URLs carry
`?v=__ASSET_VERSION__`, replaced at build.
- `assets/static/js/timer.ts` **pure, exported, unit-tested** helpers and types
- `assets/static/js/timer.ts`: **pure, exported, unit-tested** helpers and types
(`parseTarget`, `tzOffsetMs`, `computeState`, `splitDuration`, `pad2`). This is
where the date/time-zone math lives; keep it framework-free and side-effect-free.
- `assets/static/js/main.ts` the browser **entry**. Reads the query string,
- `assets/static/js/main.ts`: the browser **entry**. Reads the query string,
resolves the target once, and ticks the four units + direction + target line +
message on a second-aligned loop. Keep it **export-free** and free of top-level
`await`.
- `.well-known/signage-app.json` the app-store manifest (settings schema +
- `.well-known/signage-app.json`: the app-store manifest (settings schema +
launch template). `test/manifest.test.ts` validates it.

`build.js` builds into `dist/` **without mutating sources**: vendor fonts → copy
Expand All @@ -69,9 +69,9 @@ minify the TS → stamp a sha256 content hash into `?v=` URLs → write `CNAME`
two-pass `tzOffsetMs` (via `Intl` tz data) that stays correct across DST.

`tzOffsetMs` uses `Intl.DateTimeFormat` with an explicit `timeZone`, so results
don't depend on the host's local zone the unit tests assert exact UTC instants.
don't depend on the host's local zone; the unit tests assert exact UTC instants.

## Design "Countdown"
## Design: "Countdown"

Big tabular Bricolage Grotesque numerals over a graphite ground with one mint
accent; four labelled units on a row (wrapping to 2×2 when narrow), the title
Expand All @@ -93,6 +93,8 @@ reduced-motion gating beyond the one-off entrance.

## Deploy

Push to **`master`** → `.github/workflows/deploy-pages.yml` builds and publishes
to Pages. PRs run `ci.yml` (typecheck + lint + test + build). Action versions are
SHA-pinned.
Deploys are **tag-driven**: pushing a CalVer tag (`YYYY.M.PATCH`, e.g.
`2026.8.0`) runs `.github/workflows/deploy-pages.yml`, which builds and publishes
to Pages; it also accepts `workflow_dispatch`. Pushing to `master` on its own
does **not** deploy. PRs run `ci.yml` (typecheck + lint + test + build). Action
versions are SHA-pinned.
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Screenly Timer App

A full-screen **countdown / count-up** timer for digital signage. It ticks the
days, hours, minutes and seconds to a target instant then keeps going and
days, hours, minutes and seconds to a target instant, then keeps going and
counts **up** the elapsed time once the target passes. The same screen handles a
launch countdown *and* a "days since" board. Big tabular Bricolage Grotesque
numerals over a graphite ground with a single mint accent.
Expand All @@ -12,7 +12,7 @@ Live: **https://timer.srly.io**

Part of the Screenly signage family alongside the [quotes](../quotes),
[opening-hours](../opening-hours) and [world-clock](../world-clock) apps. Like
Quotes, this is a fully **static** site hosted on **GitHub Pages** there's no
Quotes, this is a fully **static** site hosted on **GitHub Pages**; there's no
server; the clock ticks entirely in the browser. Like Opening Hours it takes
**settings**: the target arrives in the launch URL's query string, so one
deployment times any event.
Expand All @@ -33,7 +33,7 @@ https://timer.srly.io/?title=Product+Launch&target=2026-12-31T23:59:59&tz=Europe
| `message` | Optional line shown once the target is reached (during count-up), e.g. `Happy New Year!`. |

Opened with no parameters (e.g. the store preview), it counts down to a worked
example so the screen is never blank. There's no data to refresh it's a single
example so the screen is never blank. There's no data to refresh; it's a single
self-ticking page.

## Direction is automatic
Expand Down Expand Up @@ -67,7 +67,7 @@ Requires [Bun](https://bun.sh). Never npm/npx.
bun install # deps; vendored fonts come from @fontsource via sync-fonts
bun run dev # build + serve dist/ locally
bun run build # assemble dist/ for GitHub Pages
bun test # bun:test date math + manifest validation
bun test # bun:test for date math + manifest validation
bun run typecheck
bun run lint
```
Expand All @@ -80,9 +80,11 @@ bun run lint
write `CNAME` (`timer.srly.io`). `dist/` is gitignored and is the artifact GitHub
Pages publishes.

Push to **`master`** and `.github/workflows/deploy-pages.yml` builds and deploys
to Pages. Pull requests run `ci.yml` (typecheck + lint + test + build). Action
versions are SHA-pinned.
Deploys are **tag-driven**: pushing a CalVer tag (`YYYY.M.PATCH`, e.g.
`2026.8.0`) runs `.github/workflows/deploy-pages.yml`, which builds and deploys
to Pages; it also accepts `workflow_dispatch`. Pushing to **`master`** on its own
does **not** deploy. Pull requests run `ci.yml` (typecheck + lint + test +
build). Action versions are SHA-pinned.

## Licence

Expand Down
4 changes: 2 additions & 2 deletions assets/static/js/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const targetLabel = targetMs === null ? '' : formatTarget(targetMs, tz)

document.title = title === 'Countdown' ? 'Timer' : `${title} | Timer`

// Paint one frame from the current clock. Returns nothing it's called on a
// Paint one frame from the current clock. Returns nothing; it's called on a
// timer. When the target can't be parsed we show a dashed placeholder and stop.
const paint = (): void => {
if (targetMs === null) {
Expand Down Expand Up @@ -97,7 +97,7 @@ const render = (): void => {
setLine('target-line', 'Set a target date to start the timer.')
} else {
// Report once per load whether this timer is counting down to a future
// target or up from a past one the page reloads periodically, so each
// target or up from a past one; the page reloads periodically, so each
// load re-reports the current mode.
const { direction } = computeState(targetMs, Date.now())
track('timer_direction', { direction: direction === 'down' ? 'countdown' : 'countup' })
Expand Down
4 changes: 2 additions & 2 deletions assets/static/js/timer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// they can be unit-tested with `bun:test`; main.ts is the (untestable, no-exports)
// browser entry that wires these into the DOM and ticks the clock.
//
// The app takes no dataset a single timer is described entirely by the launch
// The app takes no dataset; a single timer is described entirely by the launch
// URL's query string (see .well-known/signage-app.json): a target instant, plus
// an optional title, time zone, and message. Direction is automatic: a future
// target counts down; once it passes (or a past target) it counts up the elapsed
Expand Down Expand Up @@ -44,7 +44,7 @@ export const computeState = (targetMs: number, nowMs: number): TimerState => {
}
}

// The offset, in ms, of a named IANA time zone at a given instant i.e. how far
// The offset, in ms, of a named IANA time zone at a given instant, i.e. how far
// that zone's wall-clock is ahead of UTC (e.g. +3600000 for CET in winter). Uses
// the Intl tz database, which is always available, so results don't depend on the
// host's local zone. Returns 0 for an unknown/empty zone (treated as UTC).
Expand Down
14 changes: 7 additions & 7 deletions assets/static/styles/tailwind.css
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
/* =========================================================================
Screenly Timer App "Countdown"
Big ticking days / hours / minutes / seconds counting down toor up from
Screenly Timer App: "Countdown"
Big ticking days / hours / minutes / seconds counting down to, or up from,
a target instant, set in a tabular Bricolage Grotesque over a graphite ground
with a single mint accent. The clock is the hero: four labelled numerals on a
row, an event title above and the target date below. Tuned for unattended
signage legible at distance, no interaction, both orientations. The seconds
signage: legible at distance, no interaction, both orientations. The seconds
flip is the only motion, so there's nothing to gate for reduced-motion.
========================================================================= */

@import 'tailwindcss';
/* Shared base (brand/font tokens + fluid root + the degraded-mode kill-switch),
the canonical @font-face set, the standardized footer badge, and the .stage
frame all from @screenly-labs/signage-kit. The app's own @theme + component
frame, all from @screenly-labs/signage-kit. The app's own @theme + component
styles below layer on top; the design identity stays here, per-app. */
@import '@screenly-labs/signage-kit/styles/preset.css';
@import '@screenly-labs/signage-kit/styles/fonts.css';
@import '@screenly-labs/signage-kit/styles/brand.css';
@import '@screenly-labs/signage-kit/styles/stage.css';

/* Design tokens also generate utilities (bg-ink, text-mint, font-display…).
/* Design tokens; also generate utilities (bg-ink, text-mint, font-display…).
--font-sans (Hanken Grotesk) comes from the kit preset; --font-display is
overridden to Bricolage Grotesque, this app's grotesque display face. */
@theme {
Expand Down Expand Up @@ -187,9 +187,9 @@
}

/* =========================================================================
Degraded mode old/weak signage players (html.legacy set by the shared gate).
Degraded mode: old/weak signage players (html.legacy set by the shared gate).
The generic kill-switch (drops animation/transition/will-change) now comes from
the kit's preset, imported above. The clock ticks in JS and the only CSS motion
is the one-off .timer entrance, which the kill-switch already disables there's
is the one-off .timer entrance, which the kill-switch already disables, so there's
no animated resting state to hold, so this app needs no extra html.legacy rule.
========================================================================= */
2 changes: 1 addition & 1 deletion build.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bun
/* global Bun */
// Builds the static site into ./dist for GitHub Pages. Like the Quotes and
// Opening Hours apps this is a plain static bundle no server. The timer has no
// Opening Hours apps this is a plain static bundle, no server. The timer has no
// dataset: the title and target arrive entirely in the launch URL's query string
// (see the manifest at .well-known/signage-app.json), and the clock ticks live in
// the browser. Steps:
Expand Down
2 changes: 1 addition & 1 deletion sync-fonts.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bun
// Vendor this app's webfonts into ./assets/static/fonts. The files, versions,
// and copy logic all live in @screenly-labs/signage-kit this just names the
// and copy logic all live in @screenly-labs/signage-kit; this just names the
// families the "Countdown" design uses (Bricolage Grotesque display + Hanken
// Grotesk for the labels and target line).

Expand Down