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
5 changes: 3 additions & 2 deletions assets/static/js/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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 so the shim is in
// place before any render.
import '@screenly-labs/signage-kit/polyfills'
import qrcode from 'qrcode-generator'
import { hostLabel, largestFit, relativeTime } from './render'
// The wire contract is defined once in the worker's parser. `import type` is
Expand Down
23 changes: 0 additions & 23 deletions assets/static/js/polyfills.ts

This file was deleted.

22 changes: 7 additions & 15 deletions assets/static/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -596,19 +596,11 @@ body {
}

/* =========================================================================
Degraded mode — old/weak signage players (html.legacy set by the inline gate
in Layout.tsx). Assume the device can't afford animation; drop it entirely.
All motion here is gated behind prefers-reduced-motion: no-preference with no
base opacity:0, so killing it leaves every element at its visible resting
state -- no reveal mirror is needed.
Degraded mode — old/weak signage players (html.legacy set by the shared gate
in Layout.tsx). The generic animation/transition/will-change kill-switch is
prepended by @screenly-labs/signage-kit at build time (includeDegraded), so it
lives in the package, not here. This app has no app-specific html.legacy rules;
all motion is gated behind prefers-reduced-motion: no-preference with no base
opacity:0, so the shared kill-switch alone leaves every element at its visible
resting state.
========================================================================= */
html.legacy *,
html.legacy *::before,
html.legacy *::after {
/* biome-ignore lint/complexity/noImportantStyles: kill-switch must override the entrance 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;
}
49 changes: 14 additions & 35 deletions build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,60 +17,39 @@
// working-tree CSS stays unminified for editing).

import { Glob } from 'bun'
import browserslist from 'browserslist'
import { build as esbuild } from 'esbuild'
import { browserslistToTargets, transform as lightningcss } from 'lightningcss'
import { bundleJs, processCss } from '@screenly-labs/signage-kit/build'
import { run as syncFonts } from './sync-fonts'

const clientOnly = process.argv.includes('--client')

// 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 Layout.tsx / main.css.
const cssTargets = browserslistToTargets(browserslist())

// Vendor the Bun-managed webfonts into ./assets first.
await syncFonts()

// ---- Client JS bundle: main.ts -> main.js --------------------------------
// esbuild bundles main.ts (inlining ./render, qrcode-generator, and the polyfills
// shim), lowers modern syntax (?., ??, spread) to the ES2017 floor so old engines
// can parse it, and emits an IIFE so the output stays a self-contained self-
// executing classic script loadable from a plain <script>.
// @screenly-labs/signage-kit bundles main.ts (inlining ./render, qrcode-generator,
// and the shared polyfills shim), lowers modern syntax (?., ??, spread) to the
// shared ES2017 floor so old engines can parse it, and emits an IIFE so the output
// stays a self-contained self-executing classic script loadable from a plain
// <script>.
try {
await esbuild({
entryPoints: ['assets/static/js/main.ts'],
bundle: true,
minify: true,
format: 'iife',
target: ['es2017'],
outfile: 'assets/static/js/main.js'
})
await bundleJs('assets/static/js/main.ts', 'assets/static/js/main.js')
} catch (error) {
console.error('✗ Failed to build assets/static/js/main.ts')
console.error(error)
process.exit(1)
}
console.log('✓ JS: assets/static/js/main.js (esbuild, iife, es2017)')
console.log('✓ JS: assets/static/js/main.js (iife, es2017)')

// ---- CSS: down-level + minify in place (skipped for --client) ------------
// Lightning CSS down-levels the authored CSS to the browserslist floor and
// minifies, writing back in place. url(/static/...) refs are left untouched.
// @screenly-labs/signage-kit down-levels the authored CSS to the shared floor,
// prepends the shared html.legacy kill-switch (includeDegraded), and minifies,
// writing back in place. url(/static/...) refs are left untouched.
if (!clientOnly) {
const cssEntries: string[] = []
for await (const path of new Glob('assets/static/styles/*.css').scan('.')) {
cssEntries.push(path)
}

for (const path of cssEntries) {
try {
const { code } = lightningcss({
filename: path,
code: await Bun.file(path).bytes(),
minify: true,
targets: cssTargets
const code = await processCss(await Bun.file(path).text(), {
includeDegraded: true,
filename: path
})
await Bun.write(path, code)
} catch (error) {
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.

7 changes: 1 addition & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,11 @@
"deploy": "wrangler deploy --env=dev"
},
"license": "AGPL-3.0-only",
"browserslist": [
"chrome >= 87",
"firefox >= 78",
"safari >= 14.1",
"edge >= 87"
],
"dependencies": {
"@fontsource-variable/bricolage-grotesque": "^5.2.10",
"@fontsource-variable/hanken-grotesk": "^5.2.8",
"@fontsource/space-mono": "^5.2.9",
"@screenly-labs/signage-kit": "github:Screenly-Labs/signage-kit#2026.7.0",
"hono": "^4.12.27",
"qrcode-generator": "^2.0.4"
},
Expand Down
22 changes: 4 additions & 18 deletions src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { html, raw } from 'hono/html'
import { GATE } from '@screenly-labs/signage-kit/gate'
import type { Child } from 'hono/jsx'

interface LayoutProps {
Expand Down Expand Up @@ -71,24 +72,9 @@ const Layout = (props: LayoutProps) => html`<!DOCTYPE html>
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>
<!-- Shared degraded-mode gate from @screenly-labs/signage-kit, before the
stylesheet so html.legacy is set on the first paint. -->
${raw(GATE)}
<link rel="stylesheet" href="/static/styles/main.css?v=${props.v}" />
${sentryScript(props.sentryId)}
${gaScript(props.gaId, props.feedId, props.feedTitle)}
Expand Down
Loading