@screenly/edge-apps 1.4.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 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
@@ -201,6 +205,29 @@ async function loadWeather() {
201
205
  }
202
206
  ```
203
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
+
204
231
  ## Web Components
205
232
 
206
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
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './calendar.js';
2
+ export * from './color.js';
2
3
  export * from './error-handling.js';
3
4
  export * from './edge-app-cache.js';
4
5
  export * from './html.js';
@@ -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,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"}
@@ -1,4 +1,5 @@
1
1
  export * from './calendar.js';
2
+ export * from './color.js';
2
3
  export * from './error-handling.js';
3
4
  export * from './edge-app-cache.js';
4
5
  export * from './html.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@screenly/edge-apps",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "A TypeScript library for interfacing with Screenly Edge Apps API",
5
5
  "type": "module",
6
6
  "sideEffects": [