@screenly/edge-apps 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -0
- package/dist/utils/color.d.ts +14 -0
- package/dist/utils/color.d.ts.map +1 -0
- package/dist/utils/color.js +72 -0
- package/dist/utils/edge-app-cache.d.ts +16 -0
- package/dist/utils/edge-app-cache.d.ts.map +1 -0
- package/dist/utils/edge-app-cache.js +49 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -105,6 +105,10 @@ signalReady()
|
|
|
105
105
|
- `setupBrandingLogo()` - Fetch and process branding logo
|
|
106
106
|
- `setupBranding()` - Setup complete branding (colors and logo)
|
|
107
107
|
|
|
108
|
+
### Color
|
|
109
|
+
|
|
110
|
+
- `isLightColor(color)` - Check whether a color needs dark text on top of it
|
|
111
|
+
|
|
108
112
|
### Settings
|
|
109
113
|
|
|
110
114
|
- `getSettings()` - Get all settings
|
|
@@ -140,12 +144,90 @@ signalReady()
|
|
|
140
144
|
- `addUTMParams(url, params?)` - Add UTM parameters to URL
|
|
141
145
|
- `addUTMParamsIf(url, enabled, params?)` - Conditionally add UTM parameters
|
|
142
146
|
|
|
147
|
+
### Edge App Cache
|
|
148
|
+
|
|
149
|
+
- `readEdgeAppCache(namespace, key)` - Read a `localStorage`-backed, namespaced last-known-good value
|
|
150
|
+
- `writeEdgeAppCache(namespace, key, value)` - Write a `localStorage`-backed, namespaced last-known-good value
|
|
151
|
+
|
|
143
152
|
### Error Reporting (Sentry)
|
|
144
153
|
|
|
145
154
|
- `setupSentry(app, contexts?)` - Initialize Sentry using the `sentry_dsn` setting; sets the `edge_app` tag, hostname, and any additional contexts. No-ops if `sentry_dsn` is not configured.
|
|
146
155
|
- `scrubSensitiveData(event)` - Sentry `beforeSend` hook that redacts values of settings keys matching `token`, `secret`, `password`, or `credential` with `[REDACTED]`. Drops the event if it cannot be safely serialized.
|
|
147
156
|
- `reportError(error, context?)` - Capture an exception via Sentry with optional extra context.
|
|
148
157
|
|
|
158
|
+
## Edge App Cache
|
|
159
|
+
|
|
160
|
+
When an Edge App fetches data from a backend, a failure shouldn't necessarily
|
|
161
|
+
break the display — falling back to the last-known-good value is often better
|
|
162
|
+
than showing an error.
|
|
163
|
+
|
|
164
|
+
### `readEdgeAppCache(namespace, key)` / `writeEdgeAppCache(namespace, key, value)`
|
|
165
|
+
|
|
166
|
+
Read/write a `localStorage`-backed value under `namespace`, so different
|
|
167
|
+
apps/caches don't collide. There is no TTL: it's meant to store the
|
|
168
|
+
last-known-good value and be consulted only after a genuine fetch failure, not
|
|
169
|
+
as a general-purpose expiring cache — and `localStorage` itself isn't
|
|
170
|
+
guaranteed to survive a device reboot. Both fail silently (return `null` /
|
|
171
|
+
no-op) if storage is unavailable, disabled, or full — so it's safe to use
|
|
172
|
+
without extra error handling around it.
|
|
173
|
+
|
|
174
|
+
`writeEdgeAppCache()` accepts any JSON-serializable value (object, array,
|
|
175
|
+
string, number, etc.) — `WeatherData` below is just a stand-in name for
|
|
176
|
+
"whatever your fetch returns," not a type this library exports.
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import {
|
|
180
|
+
readEdgeAppCache,
|
|
181
|
+
writeEdgeAppCache,
|
|
182
|
+
getSettingWithDefault,
|
|
183
|
+
} from '@screenly/edge-apps'
|
|
184
|
+
|
|
185
|
+
async function loadWeather() {
|
|
186
|
+
const displayErrors = getSettingWithDefault('display_errors', false)
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const response = await fetch(weatherApiUrl)
|
|
190
|
+
if (!response.ok) throw new Error(`Weather API returned ${response.status}`)
|
|
191
|
+
|
|
192
|
+
// weatherData can be any JSON-serializable shape, e.g.:
|
|
193
|
+
// { temperature: 18, description: 'Cloudy', unit: 'metric' }
|
|
194
|
+
const weatherData: WeatherData = await response.json()
|
|
195
|
+
writeEdgeAppCache('my-edge-app', 'weather', weatherData)
|
|
196
|
+
return weatherData
|
|
197
|
+
} catch (error) {
|
|
198
|
+
// When `display_errors` is on, the raw error always wins, by design, so
|
|
199
|
+
// operators can diagnose real problems instead of seeing stale data.
|
|
200
|
+
if (!displayErrors) {
|
|
201
|
+
return readEdgeAppCache<WeatherData>('my-edge-app', 'weather')
|
|
202
|
+
}
|
|
203
|
+
throw error
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Color Contrast
|
|
209
|
+
|
|
210
|
+
Customers supply their own accent color through `screenly_color_accent`, so an
|
|
211
|
+
app that paints anything on top of it cannot hardcode the text color. A pale
|
|
212
|
+
brand needs dark text, a deep one needs light text.
|
|
213
|
+
|
|
214
|
+
`isLightColor()` answers that question using the WCAG relative luminance
|
|
215
|
+
formula, so hues are ranked the way an eye ranks them rather than by averaging
|
|
216
|
+
raw channels. Pure yellow and pure blue have similar RGB totals but land on
|
|
217
|
+
opposite sides of the threshold.
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { isLightColor, setupTheme } from '@screenly/edge-apps'
|
|
221
|
+
|
|
222
|
+
const { primary } = setupTheme()
|
|
223
|
+
document.body.classList.toggle('on-light-brand', isLightColor(primary))
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
It accepts hex (`#abc`, `#aabbcc`, `#aabbccdd`) and numeric `rgb()` / `rgba()`
|
|
227
|
+
strings, ignoring any alpha. Percentage channels and named colors are not
|
|
228
|
+
supported. Anything it cannot parse returns `false`, so a malformed setting
|
|
229
|
+
leaves an unattended screen rendering instead of throwing.
|
|
230
|
+
|
|
149
231
|
## Web Components
|
|
150
232
|
|
|
151
233
|
This library includes reusable web components for building consistent Edge Apps. See the [components documentation](https://github.com/Screenly/edge-apps-library/blob/main/docs/components.md) for usage details.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check whether a color is light enough to need dark text on top of it.
|
|
3
|
+
*
|
|
4
|
+
* Uses the WCAG relative luminance formula rather than raw RGB averages, so
|
|
5
|
+
* hues of the same nominal brightness are ranked the way an eye ranks them.
|
|
6
|
+
* Accepts hex (`#abc`, `#aabbcc`, `#aabbccdd`) and numeric `rgb()` / `rgba()`
|
|
7
|
+
* strings; any alpha component is ignored. Percentage channels and named
|
|
8
|
+
* colors are not supported.
|
|
9
|
+
*
|
|
10
|
+
* Returns `false` for values it cannot parse, so an unattended screen keeps
|
|
11
|
+
* rendering instead of throwing on a malformed setting.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isLightColor(color: string): boolean;
|
|
14
|
+
//# sourceMappingURL=color.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"color.d.ts","sourceRoot":"","sources":["../../src/utils/color.ts"],"names":[],"mappings":"AAkEA;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAKnD"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relative luminance at which text contrast against black and against white
|
|
3
|
+
* is equal. Colors above it read better with dark text, colors below it with
|
|
4
|
+
* light text.
|
|
5
|
+
*/
|
|
6
|
+
const LIGHT_THRESHOLD = 0.179;
|
|
7
|
+
function clampChannel(value) {
|
|
8
|
+
return Math.min(255, Math.max(0, value));
|
|
9
|
+
}
|
|
10
|
+
function parseHex(color) {
|
|
11
|
+
const digits = color.slice(1);
|
|
12
|
+
const expanded = digits.length === 3 || digits.length === 4
|
|
13
|
+
? digits.replace(/./g, (digit) => digit + digit)
|
|
14
|
+
: digits;
|
|
15
|
+
if (expanded.length !== 6 && expanded.length !== 8)
|
|
16
|
+
return null;
|
|
17
|
+
if (!/^[0-9a-f]+$/i.test(expanded))
|
|
18
|
+
return null;
|
|
19
|
+
return [
|
|
20
|
+
parseInt(expanded.slice(0, 2), 16),
|
|
21
|
+
parseInt(expanded.slice(2, 4), 16),
|
|
22
|
+
parseInt(expanded.slice(4, 6), 16),
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
function parseRgb(color) {
|
|
26
|
+
const match = color.match(/^rgba?\(([^)]+)\)$/i);
|
|
27
|
+
if (!match)
|
|
28
|
+
return null;
|
|
29
|
+
const channels = match[1]
|
|
30
|
+
.split(/[\s,/]+/)
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.slice(0, 3)
|
|
33
|
+
.map(Number);
|
|
34
|
+
if (channels.length !== 3 || channels.some(Number.isNaN))
|
|
35
|
+
return null;
|
|
36
|
+
return [
|
|
37
|
+
clampChannel(channels[0]),
|
|
38
|
+
clampChannel(channels[1]),
|
|
39
|
+
clampChannel(channels[2]),
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
function parseColor(color) {
|
|
43
|
+
const value = color.trim();
|
|
44
|
+
if (value.startsWith('#'))
|
|
45
|
+
return parseHex(value);
|
|
46
|
+
return parseRgb(value);
|
|
47
|
+
}
|
|
48
|
+
function toLinear(channel) {
|
|
49
|
+
const value = channel / 255;
|
|
50
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
51
|
+
}
|
|
52
|
+
function getRelativeLuminance([red, green, blue]) {
|
|
53
|
+
return (0.2126 * toLinear(red) + 0.7152 * toLinear(green) + 0.0722 * toLinear(blue));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Check whether a color is light enough to need dark text on top of it.
|
|
57
|
+
*
|
|
58
|
+
* Uses the WCAG relative luminance formula rather than raw RGB averages, so
|
|
59
|
+
* hues of the same nominal brightness are ranked the way an eye ranks them.
|
|
60
|
+
* Accepts hex (`#abc`, `#aabbcc`, `#aabbccdd`) and numeric `rgb()` / `rgba()`
|
|
61
|
+
* strings; any alpha component is ignored. Percentage channels and named
|
|
62
|
+
* colors are not supported.
|
|
63
|
+
*
|
|
64
|
+
* Returns `false` for values it cannot parse, so an unattended screen keeps
|
|
65
|
+
* rendering instead of throwing on a malformed setting.
|
|
66
|
+
*/
|
|
67
|
+
export function isLightColor(color) {
|
|
68
|
+
const rgb = parseColor(color);
|
|
69
|
+
if (!rgb)
|
|
70
|
+
return false;
|
|
71
|
+
return getRelativeLuminance(rgb) > LIGHT_THRESHOLD;
|
|
72
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads and JSON-parses the value stored under `${namespace}:${key}` in
|
|
3
|
+
* `localStorage`, or `null` if missing, unreadable, or storage is
|
|
4
|
+
* unavailable. Meant for last-known-good fallback data for Edge Apps:
|
|
5
|
+
* consult it only after a genuine fetch failure, not as a general-purpose
|
|
6
|
+
* expiring cache — there is no TTL, and `localStorage` itself isn't
|
|
7
|
+
* guaranteed to survive a device reboot.
|
|
8
|
+
*/
|
|
9
|
+
export declare function readEdgeAppCache<T>(namespace: string, key: string): T | null;
|
|
10
|
+
/**
|
|
11
|
+
* JSON-serializes and stores `value` under `${namespace}:${key}` in
|
|
12
|
+
* `localStorage`. Fails silently if storage is unavailable, disabled, or
|
|
13
|
+
* full.
|
|
14
|
+
*/
|
|
15
|
+
export declare function writeEdgeAppCache(namespace: string, key: string, value: unknown): void;
|
|
16
|
+
//# sourceMappingURL=edge-app-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edge-app-cache.d.ts","sourceRoot":"","sources":["../../src/utils/edge-app-cache.ts"],"names":[],"mappings":"AASA;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAU5E;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,OAAO,GACb,IAAI,CAWN"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
function getStorage() {
|
|
2
|
+
try {
|
|
3
|
+
if (typeof localStorage === 'undefined')
|
|
4
|
+
return null;
|
|
5
|
+
return localStorage;
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Reads and JSON-parses the value stored under `${namespace}:${key}` in
|
|
13
|
+
* `localStorage`, or `null` if missing, unreadable, or storage is
|
|
14
|
+
* unavailable. Meant for last-known-good fallback data for Edge Apps:
|
|
15
|
+
* consult it only after a genuine fetch failure, not as a general-purpose
|
|
16
|
+
* expiring cache — there is no TTL, and `localStorage` itself isn't
|
|
17
|
+
* guaranteed to survive a device reboot.
|
|
18
|
+
*/
|
|
19
|
+
export function readEdgeAppCache(namespace, key) {
|
|
20
|
+
const storage = getStorage();
|
|
21
|
+
if (!storage)
|
|
22
|
+
return null;
|
|
23
|
+
try {
|
|
24
|
+
const raw = storage.getItem(`${namespace}:${key}`);
|
|
25
|
+
return raw ? JSON.parse(raw) : null;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* JSON-serializes and stores `value` under `${namespace}:${key}` in
|
|
33
|
+
* `localStorage`. Fails silently if storage is unavailable, disabled, or
|
|
34
|
+
* full.
|
|
35
|
+
*/
|
|
36
|
+
export function writeEdgeAppCache(namespace, key, value) {
|
|
37
|
+
const storage = getStorage();
|
|
38
|
+
if (!storage)
|
|
39
|
+
return;
|
|
40
|
+
try {
|
|
41
|
+
const serialized = JSON.stringify(value);
|
|
42
|
+
if (serialized === undefined)
|
|
43
|
+
return;
|
|
44
|
+
storage.setItem(`${namespace}:${key}`, serialized);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Storage disabled or quota exceeded; caller simply has nothing cached.
|
|
48
|
+
}
|
|
49
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA;AAC7B,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,UAAU,CAAA;AACxB,cAAc,eAAe,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,UAAU,CAAA;AACxB,cAAc,eAAe,CAAA"}
|
package/dist/utils/index.js
CHANGED