@lightningjs/blits 2.8.9 → 2.10.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/CHANGELOG.md CHANGED
@@ -1,7 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.10.0
4
+
5
+ _21 sep 2026_
6
+
7
+ - Fixed / improved type definitions
8
+ - Fixed issue with accidental sideeffect running when `$hasFocus` triggers after destroy
9
+ - Fixed issue where an error in an effect can break execution of other effects
10
+ - Added support for reactivity of `array.length`
11
+ - Improved reactivity in the case of nested array operations
12
+ - Fixed case where transition callbacks could run after destroy
13
+ - Fixed case where keyup callback could fire after component destroy
14
+ - Improved error handling and parse rejection for fetchJson method
15
+ - Fixed typing of `fpsUpdate`
16
+ - Added textCalculated listener to trigger layout changes
17
+ - Added `mouseMoveThrottle` setting to configure responsiveness of mouse movements
18
+ - Fixed issue where hover is not recognized after keyboard use
19
+ - Added support for numeric rgba colors
20
+ - Bumped renderer to 3.5.0
21
+
22
+ ## 2.9.0
23
+
24
+ _9 sep 2026_
25
+
26
+ - Added DOM based Aria announcer implementation
27
+
28
+
3
29
  ## 2.8.9
4
30
 
31
+ _12 aug 2026_
32
+
5
33
  - Fixed focus issue related to multiple named router views
6
34
 
7
35
  ## 2.8.8
package/index.d.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  // blits file type reference
19
19
  /// <reference path="./blits.d.ts" />
20
20
 
21
- import {type ShaderEffect as RendererShaderEffect, type RendererMainSettings} from '@lightningjs/renderer'
21
+ import {type ShaderEffect as RendererShaderEffect, type RendererMainSettings, type FrameCounter} from '@lightningjs/renderer'
22
22
  import { CanvasShaderType } from '@lightningjs/renderer/canvas';
23
23
  import { WebGlShaderType } from '@lightningjs/renderer/webgl';
24
24
 
@@ -242,7 +242,7 @@ declare module '@lightningjs/blits' {
242
242
  *
243
243
  * Note: This event fire multiple times
244
244
  */
245
- fpsUpdate?: (fps: number) => void;
245
+ fpsUpdate?: (fps: number, frameCounter: FrameCounter) => void;
246
246
  }
247
247
 
248
248
  export interface Input {
@@ -642,7 +642,9 @@ declare module '@lightningjs/blits' {
642
642
  type InferProp<T> = T extends (...args: any[]) => any ? ReturnType<T> : T
643
643
 
644
644
  type InferProps<T extends Record<string, any>> = {
645
- [K in keyof T]: InferProp<T[K]>
645
+ // Since InferProps is no longer used for `Computed` props,
646
+ // then we do not need to infer the return type of a function-typed prop in `Props`.
647
+ [K in keyof T]: T[K]
646
648
  }
647
649
 
648
650
  type Computed<T extends Record<string, () => any>> = {
@@ -653,7 +655,7 @@ declare module '@lightningjs/blits' {
653
655
  P extends Record<string, any>,
654
656
  S,
655
657
  M,
656
- C extends Record<string, () => any>,
658
+ C extends Record<string, () => any> = Record<never, never>,
657
659
  > = ThisType<
658
660
  Readonly<InferProps<P>> & S & M & Computed<C> & ChildComponentBase
659
661
  >
@@ -662,14 +664,14 @@ declare module '@lightningjs/blits' {
662
664
  P extends Record<string, any>,
663
665
  S,
664
666
  M,
665
- C,
667
+ C extends Record<string, () => any> = Record<never, never>,
666
668
  > = ThisType<Readonly<InferProps<P>> & S & M & Computed<C> & ApplicationBase>
667
669
 
668
670
  export interface ComponentConfig<
669
671
  P extends Props = {},
670
672
  S,
671
673
  M,
672
- C extends Record<string, () => any>,
674
+ C extends Record<string, () => any> = Record<never, never>,
673
675
  W,
674
676
  > {
675
677
  components?: {
@@ -1408,6 +1410,14 @@ declare module '@lightningjs/blits' {
1408
1410
  */
1409
1411
  enableMouse?: boolean,
1410
1412
  /**
1413
+ * Minimum time in milliseconds between two pointer moves that are processed for hover.
1414
+ *
1415
+ * Moves arriving sooner are dropped, not delayed.
1416
+ *
1417
+ * @default 100
1418
+ */
1419
+ mouseMoveThrottle?: number,
1420
+ /**
1411
1421
  * Maximum FPS at which the App will be rendered
1412
1422
  *
1413
1423
  * Lowering the maximum FPS value can improve the overall experience on lower end devices.
@@ -1478,7 +1488,7 @@ declare module '@lightningjs/blits' {
1478
1488
  P extends Props,
1479
1489
  S extends State,
1480
1490
  M extends Methods,
1481
- C extends Computed,
1491
+ C extends Computed = Record<never, never>,
1482
1492
  W extends Watch>(config: ApplicationConfig<P, S, M, C, W>) : ComponentFactory
1483
1493
  /**
1484
1494
  * Blits Component
@@ -1489,7 +1499,7 @@ declare module '@lightningjs/blits' {
1489
1499
  P extends Props,
1490
1500
  S extends State,
1491
1501
  M extends Methods,
1492
- C extends Computed,
1502
+ C extends Computed = Record<never, never>,
1493
1503
  W extends Watch>(name: string, config: ComponentConfig<P, S, M, C, W>) : ComponentFactory
1494
1504
  /**
1495
1505
  * Blits Launch
@@ -1526,4 +1536,5 @@ declare module '@lightningjs/blits' {
1526
1536
  const Blits: Blits;
1527
1537
 
1528
1538
  export default Blits;
1539
+
1529
1540
  }
package/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "2.8.9",
3
+ "version": "2.10.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
7
7
  ".": "./index.js",
8
+ "./colors": {
9
+ "types": "./src/lib/colors/htmlColors.d.ts",
10
+ "default": "./src/lib/colors/htmlColors.js"
11
+ },
8
12
  "./platforms": {
9
13
  "types": "./src/platforms.d.ts",
10
14
  "default": "./src/platforms.js"
@@ -36,6 +40,10 @@
36
40
  "types": "./src/platforms/webOS/announcer.d.ts",
37
41
  "default": "./src/platforms/webOS/announcer.js"
38
42
  },
43
+ "./platforms/dom/announcer": {
44
+ "types": "./src/platforms/dom/announcer.d.ts",
45
+ "default": "./src/platforms/dom/announcer.js"
46
+ },
39
47
  "./testing": {
40
48
  "types": "./src/testing/index.d.ts",
41
49
  "default": "./src/testing/index.js"
@@ -66,28 +74,28 @@
66
74
  "author": "Michiel van der Geest <hello@michielvandergeest.com>",
67
75
  "license": "Apache-2.0",
68
76
  "devDependencies": {
69
- "@babel/eslint-parser": "^7.26.5",
70
- "@babel/plugin-syntax-import-assertions": "^7.26.0",
71
- "@eslint/eslintrc": "^3.3.1",
72
- "@eslint/js": "^9.33.0",
73
- "c8": "^9.1.0",
74
- "eslint": "^9.33.0",
77
+ "@babel/eslint-parser": "^7.28.6",
78
+ "@babel/plugin-syntax-import-assertions": "^7.28.6",
79
+ "@eslint/eslintrc": "^3.3.4",
80
+ "@eslint/js": "^9.39.3",
81
+ "c8": "^10.1.3",
82
+ "eslint": "^9.39.3",
75
83
  "eslint-config-prettier": "^10.1.8",
76
84
  "eslint-plugin-prettier": "^5.5.4",
77
85
  "fast-glob": "^3.3.3",
78
- "global-jsdom": "24.0.0",
86
+ "global-jsdom": "^26.0.0",
79
87
  "globals": "^16.3.0",
80
88
  "husky": "^9.1.7",
81
- "jsdom": "24.0.0",
82
- "lint-staged": "^15.5.0",
89
+ "jsdom": "^26.0.0",
90
+ "lint-staged": "^15.5.2",
83
91
  "prettier": "^3.6.2",
84
- "sinon": "^21.0.0",
92
+ "sinon": "^21.0.1",
85
93
  "tap-diff": "^0.1.1",
86
94
  "tap-filter": "^1.0.3",
87
- "tape": "^5.5.0"
95
+ "tape": "^5.9.0"
88
96
  },
89
97
  "dependencies": {
90
- "@lightningjs/renderer": "3.3.1",
98
+ "@lightningjs/renderer": "3.5.0",
91
99
  "magic-string": "^0.30.21"
92
100
  },
93
101
  "repository": {
@@ -81,15 +81,30 @@ const addToQueue = (message, politeness, delay = false, options = {}) => {
81
81
 
82
82
  // setup a promise to allow developer to chain functionality
83
83
  // when specific utterances are done
84
- let resolveFn
84
+ let resolvePromise
85
+ let settled = false
85
86
  const done = new Promise((resolve) => {
86
- resolveFn = resolve
87
+ resolvePromise = resolve
87
88
  })
88
89
 
90
+ const resolveFn = (status) => {
91
+ if (settled === true) return
92
+ settled = true
93
+ resolvePromise(status)
94
+ }
95
+
89
96
  // augment the promise with a cancel / remove function
90
97
  done.remove = done.cancel = () => {
91
98
  const index = queue.findIndex((item) => item.id === id)
92
- if (index !== -1) queue.splice(index, 1)
99
+ if (index !== -1) {
100
+ queue.splice(index, 1)
101
+ } else if (id === currentId && debounce !== null) {
102
+ clearDebounceTimer()
103
+ currentId = null
104
+ currentResolveFn = null
105
+ isProcessing = false
106
+ processQueue()
107
+ }
93
108
  Log.debug(`Announcer - removed from queue: "${message}" (id: ${id})`)
94
109
  resolveFn('canceled')
95
110
  }
@@ -15,7 +15,7 @@
15
15
  * SPDX-License-Identifier: Apache-2.0
16
16
  */
17
17
 
18
- import { default as Component, componentMap } from './component.js'
18
+ import { componentMap, default as Component } from './component.js'
19
19
  import { default as Focus, keyUpCallbacks } from './focus/focus.js'
20
20
  import Hover from './focus/hover.js'
21
21
 
@@ -80,6 +80,7 @@ const Application = (config) => {
80
80
  const throttleMs = Settings.get('inputThrottle', 0)
81
81
 
82
82
  const mouseEnabled = Settings.get('enableMouse', false)
83
+ const mouseMoveThrottle = Settings.get('mouseMoveThrottle', 100)
83
84
  const { createKeyboardEvent, input, isKeyboardEvent, now = Date.now, viewport } = platform
84
85
 
85
86
  keyDownHandler = async (e) => {
@@ -114,7 +115,11 @@ const Application = (config) => {
114
115
  }
115
116
 
116
117
  Focus.input(key, e)
117
- if (mouseEnabled === true) Hover.clear()
118
+ if (mouseEnabled === true) {
119
+ Hover.clear()
120
+ // forget the node under the pointer too, so moving on it hovers it again
121
+ currentNode = undefined
122
+ }
118
123
  clearTimeout(holdTimeout)
119
124
  holdTimeout = setTimeout(
120
125
  () => {
@@ -125,10 +130,12 @@ const Application = (config) => {
125
130
  }
126
131
 
127
132
  keyUpHandler = (e) => {
128
- const cb = keyUpCallbacks.get(e.keyCode)
129
- if (cb !== undefined && typeof cb === 'function') {
133
+ const entry = keyUpCallbacks.get(e.keyCode)
134
+ if (entry !== undefined) {
130
135
  keyUpCallbacks.delete(e.keyCode)
131
- cb()
136
+ if (entry.component.eol !== true && typeof entry.callback === 'function') {
137
+ entry.callback()
138
+ }
132
139
  }
133
140
  clearTimeout(holdTimeout)
134
141
  Focus.hold = false
@@ -147,7 +154,7 @@ const Application = (config) => {
147
154
 
148
155
  // limit the amount of move events per time frame
149
156
  mouseMoveHandler = (e) => {
150
- if (e.timeStamp - lastMoved < 100) return
157
+ if (e.timeStamp - lastMoved < mouseMoveThrottle) return
151
158
  lastMoved = e.timeStamp
152
159
 
153
160
  this.$emit('mouse::move', e)
@@ -16,7 +16,12 @@
16
16
  */
17
17
 
18
18
  import symbols from '../../lib/symbols.js'
19
- import { default as Focus, keyUpCallbacks, getComponentWithInputEvent } from '../../focus/focus.js'
19
+ import {
20
+ default as Focus,
21
+ keyUpCallbacks,
22
+ getComponentWithInputEvent,
23
+ removeKeyUpCallbacks,
24
+ } from '../../focus/focus.js'
20
25
  import eventListeners from '../../lib/eventListeners.js'
21
26
  import { trigger } from '../../lib/reactivity/effect.js'
22
27
  import { Log } from '../../lib/log.js'
@@ -71,7 +76,7 @@ export default {
71
76
  }
72
77
 
73
78
  if (cb !== undefined && event.keyCode) {
74
- keyUpCallbacks.set(event.keyCode, cb)
79
+ keyUpCallbacks.set(event.keyCode, { callback: cb, component: componentWithInputEvent })
75
80
  }
76
81
 
77
82
  return true
@@ -98,14 +103,15 @@ export default {
98
103
  */
99
104
  value: function () {
100
105
  this.eol = true
106
+ removeKeyUpCallbacks(this)
101
107
  this[symbols.lifecycle].state = 'destroy'
102
108
 
109
+ removeEffects(this[symbols.effects])
110
+
103
111
  // when destroying a component that currently has focus
104
112
  // pass focus to the parent so we don't get lost in focus limbo
105
113
  if (this.$hasFocus === true) this[symbols.parent].$focus()
106
114
 
107
- removeEffects(this[symbols.effects])
108
-
109
115
  // @todo - is this really necessary?
110
116
  // This cause an issue with auto sizing of parent (and required an extra eol check there)
111
117
  for (let key in this[symbols.state]) {
@@ -275,7 +275,9 @@ const propsTransformer = {
275
275
  this.props['zIndex'] = v
276
276
  },
277
277
  set color(v) {
278
- if (typeof v === 'string' && v.startsWith('{') === false) {
278
+ if (typeof v === 'number') {
279
+ this.props['color'] = v
280
+ } else if (typeof v === 'string' && v.startsWith('{') === false) {
279
281
  this.props['color'] = colors.normalize(v)
280
282
  } else if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
281
283
  this.props['color'] = 0
@@ -649,10 +651,11 @@ const Element = {
649
651
  if (this.props.props['color'] === undefined && '__textnode' in props === false) {
650
652
  this.props.props['color'] = 0
651
653
  }
652
-
653
- this.node = props.__textnode
654
- ? renderer.createTextNode({ ...textDefaults, ...this.props.props })
655
- : renderer.createNode(this.props.props)
654
+ const isTextNode = props.__textnode
655
+ this.node =
656
+ isTextNode === true
657
+ ? renderer.createTextNode({ ...textDefaults, ...this.props.props })
658
+ : renderer.createNode(this.props.props)
656
659
 
657
660
  if (this.props['holder'] === true) {
658
661
  holderComponentMap.set(this.node, this.component)
@@ -680,7 +683,8 @@ const Element = {
680
683
 
681
684
  if (this.config.parent.props !== undefined && this.config.parent.props.__layout === true) {
682
685
  this.config.parent.triggerLayout(this.config.parent.props)
683
- this.node.on('loaded', () => {
686
+
687
+ this.node.on(isTextNode === true ? 'textCalculated' : 'loaded', () => {
684
688
  if (this.eol === true) return
685
689
  this.config.parent.triggerLayout(this.config.parent.props)
686
690
  })
@@ -809,6 +813,20 @@ const Element = {
809
813
  // @ts-ignore
810
814
  this.props.raw[prop] = value
811
815
 
816
+ if (prop === 'color' && typeof value === 'number') {
817
+ // A gradient can share its base color with the new solid color (notably zero).
818
+ // The renderer skips equal base colors, so reset its sides in that case.
819
+ if (this.node.color === value) {
820
+ this.node.colorTop = value
821
+ this.node.colorBottom = value
822
+ this.node.colorLeft = value
823
+ this.node.colorRight = value
824
+ }
825
+ // Use the renderer setter to invalidate colors, including text colors.
826
+ this.node.color = value
827
+ return
828
+ }
829
+
812
830
  this.props.props = {}
813
831
  // @ts-ignore
814
832
  this.props[prop] = unpackTransition(value)
@@ -908,6 +926,7 @@ const Element = {
908
926
  if (transition.start !== undefined && typeof transition.start === 'function') {
909
927
  // fire transition start callback when animation really starts (depending on specified delay)
910
928
  f.once('animating', () => {
929
+ if (this.eol === true) return
911
930
  transition.start.call(this.component, this, prop, startValue)
912
931
  })
913
932
  }
@@ -929,6 +948,7 @@ const Element = {
929
948
  }
930
949
 
931
950
  f.once('stopped', () => {
951
+ if (this.eol === true) return
932
952
  if (
933
953
  this.scheduledTransitions[prop] !== undefined &&
934
954
  this.scheduledTransitions[prop].canceled === true
@@ -936,7 +956,7 @@ const Element = {
936
956
  return
937
957
  }
938
958
  // fire transition end callback when animation ends (if specified)
939
- if (this.node !== undefined && transition.end && typeof transition.end === 'function') {
959
+ if (transition.end && typeof transition.end === 'function') {
940
960
  transition.end.call(this.component, this, prop, this.node[prop])
941
961
  }
942
962
  // remove the prop from scheduled transitions
@@ -1010,7 +1030,7 @@ const Element = {
1010
1030
  delete this.forComponent
1011
1031
 
1012
1032
  this.node.destroy()
1013
- this.node = null
1033
+ this.node = undefined
1014
1034
  },
1015
1035
  get nodeId() {
1016
1036
  return this.node && this.node.id
@@ -29,6 +29,12 @@ let setFocusTimeout
29
29
 
30
30
  export const keyUpCallbacks = new Map()
31
31
 
32
+ export const removeKeyUpCallbacks = (component) => {
33
+ for (const [keyCode, entry] of keyUpCallbacks) {
34
+ if (entry.component === component) keyUpCallbacks.delete(keyCode)
35
+ }
36
+ }
37
+
32
38
  export default {
33
39
  _hold: false,
34
40
  set hold(v) {
@@ -99,7 +105,7 @@ export default {
99
105
  }
100
106
 
101
107
  if (cb !== undefined) {
102
- keyUpCallbacks.set(event.keyCode, cb)
108
+ keyUpCallbacks.set(event.keyCode, { callback: cb, component: componentWithInputEvent })
103
109
  }
104
110
  },
105
111
  }
@@ -22,10 +22,18 @@ export default (file) => {
22
22
  if (xhr.readyState == XMLHttpRequest.DONE) {
23
23
  // file protocol returns 0
24
24
  // http(s) protocol returns 200
25
- if (xhr.status === 0 || xhr.status === 200) resolve(JSON.parse(xhr.responseText))
26
- else reject(xhr.statusText)
25
+ if (xhr.status === 0 || xhr.status === 200) {
26
+ try {
27
+ resolve(JSON.parse(xhr.responseText))
28
+ } catch (err) {
29
+ reject(err)
30
+ }
31
+ } else reject(xhr.statusText || xhr.status)
27
32
  }
28
33
  }
34
+
35
+ xhr.onerror = () => reject(new Error('Network request failed'))
36
+
29
37
  xhr.open('GET', file)
30
38
  xhr.send(null)
31
39
  })
@@ -15,7 +15,9 @@
15
15
  * SPDX-License-Identifier: Apache-2.0
16
16
  */
17
17
 
18
- import colorsMap from './htmlColors.js'
18
+ import htmlColors from './htmlColors.js'
19
+
20
+ const colorsMap = { ...htmlColors }
19
21
 
20
22
  const hex = /^#?([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i
21
23
  const rgba =
@@ -0,0 +1,4 @@
1
+ /** Known HTML color names mapped to hexadecimal RGBA strings, including transparent. */
2
+ declare const htmlColors: Readonly<Record<string, `0x${string}` | undefined>>
3
+
4
+ export default htmlColors
@@ -18,14 +18,17 @@
18
18
  let currentEffect = null
19
19
  let currentKey = null
20
20
 
21
- let paused = false
21
+ // counter that tracks all pauseTracking invocations
22
+ // to ensure nested array operations don't prematurely
23
+ // resume before all operations have completed
24
+ let pauseCounter = 0
22
25
 
23
26
  export const pauseTracking = () => {
24
- paused = true
27
+ pauseCounter++
25
28
  }
26
29
 
27
30
  export const resumeTracking = () => {
28
- paused = false
31
+ if (pauseCounter > 0) pauseCounter--
29
32
  }
30
33
 
31
34
  const objectMap = new WeakMap()
@@ -45,7 +48,7 @@ export const removeEffects = (effectsToRemove) => {
45
48
 
46
49
  export const track = (target, key) => {
47
50
  if (currentEffect !== null) {
48
- if (paused) {
51
+ if (pauseCounter > 0) {
49
52
  return
50
53
  }
51
54
  // note: nesting the conditions like this seems to perform better ¯\_(ツ)_/¯
@@ -76,7 +79,7 @@ export const track = (target, key) => {
76
79
  }
77
80
 
78
81
  export const trigger = (target, key, force = false) => {
79
- if (paused === true) return
82
+ if (pauseCounter > 0) return
80
83
  const effectsMap = objectMap.get(target)
81
84
  if (effectsMap === undefined) {
82
85
  return
@@ -89,10 +92,17 @@ export const trigger = (target, key, force = false) => {
89
92
  }
90
93
  }
91
94
 
92
- export const effect = (effect, key = null) => {
93
- currentEffect = effect
95
+ export const effect = (effectFn, key = null) => {
96
+ const previousEffect = currentEffect
97
+ const previousKey = currentKey
98
+
99
+ currentEffect = effectFn
94
100
  currentKey = key
95
- currentEffect()
96
- currentEffect = null
97
- currentKey = null
101
+
102
+ try {
103
+ effectFn()
104
+ } finally {
105
+ currentEffect = previousEffect
106
+ currentKey = previousKey
107
+ }
98
108
  }
@@ -67,9 +67,19 @@ const reactiveProxy = (original, _parent = null, _key) => {
67
67
  // augment array path methods (that change the length of the array)
68
68
  if (arrayPatchMethods.indexOf(key) !== -1) {
69
69
  return function (...args) {
70
+ const oldLength = target.length
70
71
  pauseTracking()
71
- const result = target[key].apply(this, args)
72
- resumeTracking()
72
+ let result
73
+ try {
74
+ result = target[key].apply(this, args)
75
+ } finally {
76
+ resumeTracking()
77
+ }
78
+ // trigger a change when the length of the array has changed
79
+ // by the array operation
80
+ if (_parent === null && target.length !== oldLength) {
81
+ trigger(target, 'length')
82
+ }
73
83
  // trigger a change on the parent object and the key
74
84
  // i.e. when pushing a new item to `obj.data`, _parent will equal `obj`
75
85
  // and _key will equal `data`
@@ -78,6 +88,10 @@ const reactiveProxy = (original, _parent = null, _key) => {
78
88
  }
79
89
  }
80
90
  if (key === 'length') {
91
+ // only track root level to prevent double reactive executions
92
+ if (_parent === null) {
93
+ track(target, 'length')
94
+ }
81
95
  return original.length
82
96
  }
83
97
 
@@ -0,0 +1,28 @@
1
+ import type { AnnouncerDriver } from '@lightningjs/blits'
2
+
3
+ export interface DOMAnnouncerOptions {
4
+ /**
5
+ * Document in which the live region is created. Defaults to the global document.
6
+ */
7
+ document?: Document | null
8
+ /**
9
+ * Element that receives the live region. Defaults to document.body.
10
+ */
11
+ container?: HTMLElement
12
+ /**
13
+ * ID used to find or create the single live region.
14
+ *
15
+ * @default 'blits-announcer'
16
+ */
17
+ id?: string
18
+ /**
19
+ * Delay between clearing the region and inserting the next message.
20
+ *
21
+ * @default 0
22
+ */
23
+ updateDelay?: number
24
+ }
25
+
26
+ declare function createAnnouncer(options?: DOMAnnouncerOptions): AnnouncerDriver
27
+
28
+ export default createAnnouncer
@@ -0,0 +1,111 @@
1
+ /*
2
+ * Copyright 2026 Comcast Cable Communications Management, LLC
3
+ * Licensed under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License.
5
+ * You may obtain a copy of the License at
6
+ *
7
+ * http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software
10
+ * distributed under the License is distributed on an "AS IS" BASIS,
11
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ * See the License for the specific language governing permissions and
13
+ * limitations under the License.
14
+ *
15
+ * SPDX-License-Identifier: Apache-2.0
16
+ */
17
+
18
+ const globalScope = globalThis
19
+ const defaultId = 'blits-announcer'
20
+
21
+ const visuallyHiddenStyles = {
22
+ position: 'absolute',
23
+ width: '1px',
24
+ height: '1px',
25
+ padding: '0',
26
+ margin: '-1px',
27
+ overflow: 'hidden',
28
+ clip: 'rect(0, 0, 0, 0)',
29
+ whiteSpace: 'nowrap',
30
+ border: '0',
31
+ }
32
+
33
+ const createAnnouncer = (options = {}) => {
34
+ const documentRef = Object.prototype.hasOwnProperty.call(options, 'document')
35
+ ? options.document
36
+ : globalScope.document
37
+ const id = options.id || defaultId
38
+ const updateDelay = options.updateDelay === undefined ? 0 : options.updateDelay
39
+
40
+ let region
41
+ let pendingUpdate
42
+
43
+ const getRegion = () => {
44
+ if (!documentRef || typeof documentRef.createElement !== 'function') return
45
+ if (region && region.isConnected !== false) return region
46
+
47
+ region = typeof documentRef.getElementById === 'function' && documentRef.getElementById(id)
48
+
49
+ if (!region) {
50
+ const parent = options.container || documentRef.body || documentRef.documentElement
51
+ if (!parent || typeof parent.appendChild !== 'function') return
52
+
53
+ region = documentRef.createElement('div')
54
+ region.id = id
55
+ parent.appendChild(region)
56
+ }
57
+
58
+ Object.assign(region.style, visuallyHiddenStyles)
59
+ region.setAttribute('role', 'alert')
60
+ region.setAttribute('aria-live', 'assertive')
61
+ region.setAttribute('aria-atomic', 'true')
62
+
63
+ return region
64
+ }
65
+
66
+ const cancelPendingUpdate = (error = 'canceled') => {
67
+ if (pendingUpdate === undefined) return
68
+ clearTimeout(pendingUpdate.timer)
69
+ pendingUpdate.reject({ error })
70
+ pendingUpdate = undefined
71
+ }
72
+
73
+ // Create the empty region before the first announcement so assistive
74
+ // technology can observe subsequent content changes reliably.
75
+ getRegion()
76
+
77
+ return {
78
+ speak(announcement) {
79
+ if (!announcement || announcement.message === undefined || announcement.message === null) {
80
+ return Promise.reject({ error: 'Missing message' })
81
+ }
82
+
83
+ const liveRegion = getRegion()
84
+ if (!liveRegion) {
85
+ return Promise.reject({ error: 'unavailable' })
86
+ }
87
+
88
+ cancelPendingUpdate()
89
+ liveRegion.textContent = ''
90
+
91
+ return new Promise((resolve, reject) => {
92
+ const update = {
93
+ reject,
94
+ timer: setTimeout(() => {
95
+ if (pendingUpdate !== update) return
96
+ liveRegion.textContent = String(announcement.message)
97
+ pendingUpdate = undefined
98
+ resolve()
99
+ }, updateDelay),
100
+ }
101
+ pendingUpdate = update
102
+ })
103
+ },
104
+ cancel() {
105
+ cancelPendingUpdate()
106
+ if (region) region.textContent = ''
107
+ },
108
+ }
109
+ }
110
+
111
+ export default createAnnouncer
@@ -55,7 +55,7 @@ export default {
55
55
  }
56
56
 
57
57
  const setDict = (language) => {
58
- return new Promise((resolve) => {
58
+ return new Promise((resolve, reject) => {
59
59
  const translationObj = translations[language]
60
60
  if (typeof translationObj === 'object') {
61
61
  dictionary = translationObj
@@ -71,6 +71,7 @@ export default {
71
71
  })
72
72
  .catch((e) => {
73
73
  Log.error(e)
74
+ reject(e)
74
75
  })
75
76
  }
76
77
  })
@@ -17,11 +17,11 @@
17
17
 
18
18
  import localCookie from './localCookie.js'
19
19
 
20
- let lc = new localCookie()
21
-
22
20
  export default {
23
21
  name: 'storage',
24
22
  plugin() {
23
+ const lc = new localCookie()
24
+
25
25
  return {
26
26
  get(key) {
27
27
  try {
@@ -111,7 +111,6 @@ export default {
111
111
  if (theme in themes) {
112
112
  themeMap = {}
113
113
  state.variant = theme
114
- console.log('change variant', theme)
115
114
  } else {
116
115
  Log.warn(`Variant ${theme} not found`)
117
116
  }