Skip to content
Merged
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
19 changes: 4 additions & 15 deletions assets/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -531,22 +531,11 @@ body {
}

/* =========================================================================
Degraded mode — old/weak signage players (html.legacy set by the inline gate
in index.html). Assume the device can't afford animation; drop it entirely.
.anim has a base opacity:0 and only appears via the entrance animation, so the
legacy block reveals it (opacity:1), mirroring the reduced-motion resting
state; the ambient breathe is then cancelled.
Degraded mode — app-specific resting state (the generic html.legacy kill-switch
is prepended at build by @screenly-labs/signage-kit). .anim has a base
opacity:0 and only appears via the entrance animation, so reveal it here
(the kill-switch cancels the animation itself), mirroring reduced-motion.
========================================================================= */
html.legacy .anim {
opacity: 1;
}
html.legacy *,
html.legacy *::before,
html.legacy *::after {
/* biome-ignore lint/complexity/noImportantStyles: kill-switch must override the entrance/ambient animation rules */
animation: none !important;
/* biome-ignore lint/complexity/noImportantStyles: kill-switch must override any transition */
transition: none !important;
/* biome-ignore lint/complexity/noImportantStyles: release GPU layer hints on weak devices */
will-change: auto !important;
}
52 changes: 19 additions & 33 deletions build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,23 @@
// project subpath like https://<org>.github.io/world-clock/.

import { cp, mkdir, rm } from 'node:fs/promises'
import browserslist from 'browserslist'
import { build as esbuild } from 'esbuild'
import { browserslistToTargets, transform as lightningcss } from 'lightningcss'
import { bundleJs, injectGate, processCss } from '@screenly-labs/signage-kit/build'
import { run as syncFonts } from './sync-fonts.ts'

const DIST = 'dist'

// The `browserslist` field in package.json is the CSS support floor: Lightning
// CSS down-levels the stylesheet to it. The JS is lowered separately by esbuild to
// a fixed ES2017 syntax floor (kept at/below the browserslist minimum); esbuild
// can't read browserslist, so keep the two in sync if you change the floor. See
// the degraded-mode notes in index.html / main.css.
const cssTargets = browserslistToTargets(browserslist())

// Vendor the Bun-managed webfonts into ./assets/fonts before copying them on.
await syncFonts()

// Start from a clean dist so removed files never linger in a deploy.
await rm(DIST, { recursive: true, force: true })
await mkdir(`${DIST}/styles`, { recursive: true })

// JS: esbuild bundles main.ts (inlining clocks.ts + the polyfills shim) into one
// minified ES module and lowers modern syntax (?., ??, spread) to the ES2017
// floor so old signage players can parse it. Kept as an ES module (the page
// still loads it with <script type="module">, supported at the floor); only
// the syntax level changes.
// CSS: Lightning CSS down-levels to the browserslist floor and minifies;
// url(../fonts/...) refs are left untouched.
// JS: kit bundler, format:'esm' so the page keeps loading it with
// <script type="module">. CSS: kit pipeline with the shared kill-switch prepended
// (the app's own @supports container-query fallback + .anim reveal stay in the CSS).
try {
await esbuild({
entryPoints: ['src/main.ts'],
bundle: true,
minify: true,
format: 'esm',
target: ['es2017'],
outfile: `${DIST}/main.js`
})
await bundleJs('src/main.ts', `${DIST}/main.js`, { format: 'esm' })
} catch (error) {
console.error('✗ JS build failed')
console.error(error)
Expand All @@ -54,22 +34,28 @@ try {
console.log(`✓ JS: ${DIST}/main.js`)

try {
const { code: cssCode } = lightningcss({
filename: 'assets/styles/main.css',
code: await Bun.file('assets/styles/main.css').bytes(),
minify: true,
targets: cssTargets
const css = await processCss(await Bun.file('assets/styles/main.css').text(), {
includeDegraded: true,
filename: 'assets/styles/main.css'
})
await Bun.write(`${DIST}/styles/main.css`, cssCode)
await Bun.write(`${DIST}/styles/main.css`, css)
} catch (error) {
console.error('✗ CSS build failed')
console.error(error)
process.exit(1)
}
console.log(`✓ CSS: ${DIST}/styles/main.css`)

// Copy the HTML shell and the vendored fonts verbatim.
await Bun.write(`${DIST}/index.html`, Bun.file('index.html'))
// Copy the HTML shell (with the shared degraded-mode gate injected) and the fonts.
// injectGate throws if it can't find the stylesheet anchor, so a template change
// that drops the gate fails the build loudly instead of shipping a gate-less page.
try {
await Bun.write(`${DIST}/index.html`, injectGate(await Bun.file('index.html').text()))
} catch (error) {
console.error('✗ Failed to inject the degraded-mode gate into index.html')
console.error(error)
process.exit(1)
}
console.log(`✓ HTML: ${DIST}/index.html`)

await cp('assets/fonts', `${DIST}/fonts`, { recursive: true })
Expand Down
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 2 additions & 18 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,8 @@
type="font/woff2"
crossorigin
/>
<!-- Degraded mode for older/weaker signage players. Runs before the
stylesheet so html.legacy is set on the first paint: flags the device as
legacy when the browser engine is old (no Element.replaceChildren, a
2020-era API) or the hardware looks weak, then the stylesheet drops all
animation. classList.add keeps it idempotent. -->
<script>
(function () {
try {
var slow =
(navigator.deviceMemory && navigator.deviceMemory <= 2) ||
(navigator.hardwareConcurrency && navigator.hardwareConcurrency <= 2)
var old = !('replaceChildren' in Element.prototype)
if (slow || old) document.documentElement.classList.add('legacy')
} catch (e) {
document.documentElement.classList.add('legacy')
}
})()
</script>
<!-- The degraded-mode gate is injected here at build time by
@screenly-labs/signage-kit (injectGate), before the stylesheet. -->
<link rel="stylesheet" href="./styles/main.css" />
<script type="module" src="./main.js"></script>
</head>
Expand Down
9 changes: 2 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,10 @@
"serve": "bun run build.ts && bunx serve dist"
},
"license": "AGPL-3.0-only",
"browserslist": [
"chrome >= 87",
"firefox >= 78",
"safari >= 14.1",
"edge >= 87"
],
"dependencies": {
"@fontsource-variable/fraunces": "^5.2.9",
"@fontsource-variable/hanken-grotesk": "^5.2.8"
"@fontsource-variable/hanken-grotesk": "^5.2.8",
"@screenly-labs/signage-kit": "github:Screenly-Labs/signage-kit#2026.7.0"
},
Comment thread
vpetersson marked this conversation as resolved.
"devDependencies": {
"@biomejs/biome": "^2.5.1",
Expand Down
4 changes: 2 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
// index.html — no framework, no server.

// Side-effect import: installs the replaceChildren shim for the older-browser
// degraded mode. Must stay first so the shim is in place before any render.
import './polyfills'
// degraded mode (shared across all apps). Must stay first.
import '@screenly-labs/signage-kit/polyfills'
import {
buildFormatters,
type ClockConfig,
Expand Down
23 changes: 0 additions & 23 deletions src/polyfills.ts

This file was deleted.

Loading