@lightningjs/blits 2.4.1 → 2.6.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,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.6.0
4
+
5
+ _25 jun 2026_
6
+
7
+ - Added platform layer for browser API abstraction (passed as a launch option)
8
+ - Improved reactivity guard vite plugin
9
+ - Added testing harness to support Blits component testing
10
+
11
+ ## 2.5.0
12
+
13
+ _11 jun 2026_
14
+
15
+ - Improved performance of Sprites
16
+ - Fixed correct typing of computed props
17
+ - Bumped renderer to 3.0.6
18
+ - Added re-export of platforms from renderer
19
+
3
20
  ## v2.4.1
4
21
 
5
22
  _02 jun 2026_
package/index.d.ts CHANGED
@@ -623,24 +623,36 @@ declare module '@lightningjs/blits' {
623
623
  [K in keyof T]: InferProp<T[K]>
624
624
  }
625
625
 
626
+ type Computed<T extends Record<string, () => any>> = {
627
+ [K in keyof T]: Readonly<ReturnType<T[K]>>
628
+ }
629
+
626
630
  export type ComponentContext<
627
631
  P extends Record<string, any>,
628
632
  S,
629
633
  M,
630
- C
631
- > = ThisType<Readonly<InferProps<P>> & S & M & Readonly<C> & ChildComponentBase>
634
+ C extends Record<string, () => any>,
635
+ > = ThisType<
636
+ Readonly<InferProps<P>> & S & M & Computed<C> & ChildComponentBase
637
+ >
632
638
 
633
639
  export type ApplicationContext<
634
640
  P extends Record<string, any>,
635
641
  S,
636
642
  M,
637
- C
638
- > = ThisType<Readonly<InferProps<P>> & S & M & Readonly<C> & ApplicationBase>
643
+ C,
644
+ > = ThisType<Readonly<InferProps<P>> & S & M & Computed<C> & ApplicationBase>
639
645
 
640
- export interface ComponentConfig<P extends Props = {}, S, M, C, W> {
646
+ export interface ComponentConfig<
647
+ P extends Props = {},
648
+ S,
649
+ M,
650
+ C extends Record<string, () => any>,
651
+ W,
652
+ > {
641
653
  components?: {
642
- [key: string]: ComponentFactory,
643
- },
654
+ [key: string]: ComponentFactory
655
+ }
644
656
  /**
645
657
  * XML-based template string of the Component
646
658
  *
@@ -1004,6 +1016,74 @@ declare module '@lightningjs/blits' {
1004
1016
  type ReactivityModes = 'Proxy' | 'defineProperty'
1005
1017
  type RenderModes = 'webgl' | 'canvas'
1006
1018
 
1019
+ interface Platform {
1020
+ /**
1021
+ * Target used for keyboard and pointer input listeners.
1022
+ */
1023
+ input?: EventTarget | any,
1024
+ /**
1025
+ * Target used for viewport-level listeners such as resize, scroll, and hashchange.
1026
+ */
1027
+ viewport?: EventTarget | any,
1028
+ /**
1029
+ * Dispatch an event through the platform's event system.
1030
+ */
1031
+ dispatchEvent?: (event: any) => boolean | void,
1032
+ /**
1033
+ * Persistent key/value storage API.
1034
+ */
1035
+ localStorage?: Storage | any,
1036
+ /**
1037
+ * Read the platform cookie string.
1038
+ */
1039
+ getCookie?: () => string,
1040
+ /**
1041
+ * Write a cookie string.
1042
+ */
1043
+ setCookie?: (value: string) => void,
1044
+ /**
1045
+ * Navigate back in the platform history, when available.
1046
+ */
1047
+ historyBack?: () => void,
1048
+ /**
1049
+ * Current viewport/screen height.
1050
+ */
1051
+ screenHeight?: number,
1052
+ /**
1053
+ * Available hardware concurrency.
1054
+ */
1055
+ hardwareConcurrency?: number,
1056
+ /**
1057
+ * User agent string for platform-specific defaults.
1058
+ */
1059
+ userAgent?: string,
1060
+ /**
1061
+ * KeyboardEvent constructor used for event creation and type checks.
1062
+ */
1063
+ KeyboardEvent?: typeof KeyboardEvent | any,
1064
+ /**
1065
+ * Check whether an object should be treated as a keyboard event.
1066
+ */
1067
+ isKeyboardEvent?: (event: any) => boolean,
1068
+ /**
1069
+ * Create a keyboard event for the current platform.
1070
+ */
1071
+ createKeyboardEvent?: (type: string, init?: KeyboardEventInit | any) => any,
1072
+ /**
1073
+ * SpeechSynthesisUtterance constructor used by the announcer.
1074
+ */
1075
+ SpeechSynthesisUtterance?: typeof SpeechSynthesisUtterance | any,
1076
+ /**
1077
+ * Speech synthesis API implementation used by the announcer.
1078
+ */
1079
+ speechSynthesis?: SpeechSynthesis | any,
1080
+ /**
1081
+ * Monotonic timestamp provider used for input throttling.
1082
+ */
1083
+ now?: () => number,
1084
+ [key: string]: any,
1085
+ }
1086
+
1007
1087
  /**
1008
1088
  * Settings
1009
1089
  *
@@ -1319,11 +1399,20 @@ declare module '@lightningjs/blits' {
1319
1399
  */
1320
1400
  maxFPS?: number,
1321
1401
  /**
1322
- * Custom platform layer to be used by the App (experimental)
1402
+ * Custom platform layer to be used by the App.
1323
1403
  *
1324
- * Defaults to `null` which will be resolved internally to WebPlatform
1404
+ * Allows replacing platform capabilities such as input targets, storage,
1405
+ * keyboard event constructors, and speech APIs.
1406
+ * A callback receives Blits' browser defaults and should return the platform overrides.
1325
1407
  */
1326
- platform?: any
1408
+ platform?: (defaults: Platform) => Partial<Platform>,
1409
+ /**
1410
+ * Custom platform layer to be used by the renderer.
1411
+ *
1412
+ * For renderer-specific platform implementations. Passing a renderer platform
1413
+ * through `platform` is deprecated.
1414
+ */
1415
+ rendererPlatform?: RendererMainSettings['platform']
1327
1416
 
1328
1417
  }
1329
1418
 
@@ -1340,7 +1429,7 @@ declare module '@lightningjs/blits' {
1340
1429
  }
1341
1430
 
1342
1431
  interface Computed {
1343
- [key: string]: any
1432
+ [key: string]: () => any
1344
1433
  }
1345
1434
 
1346
1435
 
package/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "2.4.1",
3
+ "version": "2.6.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
+ "./platforms": {
9
+ "types": "./src/platforms.d.ts",
10
+ "default": "./src/platforms.js"
11
+ },
8
12
  "./vite": "./vite/index.js",
9
13
  "./transitions": "./src/router/transitions/index.js",
10
14
  "./precompiler": "./src/lib/precompiler/precompiler.js",
@@ -28,6 +32,10 @@
28
32
  "types": "./src/plugins/storage/storage.d.ts",
29
33
  "default": "./src/plugins/storage/storage.js"
30
34
  },
35
+ "./testing": {
36
+ "types": "./src/testing/index.d.ts",
37
+ "default": "./src/testing/index.js"
38
+ },
31
39
  "./symbols": "./src/lib/symbols.js",
32
40
  "./blitsFileConverter": "./src/lib/blitsfileconverter/blitsfileconverter.js",
33
41
  "./components": "./src/components/FPScounter.js"
@@ -75,7 +83,7 @@
75
83
  "tape": "^5.5.0"
76
84
  },
77
85
  "dependencies": {
78
- "@lightningjs/renderer": "^3.0.3",
86
+ "@lightningjs/renderer": "^3.0.6",
79
87
  "magic-string": "^0.30.21"
80
88
  },
81
89
  "repository": {
@@ -17,6 +17,7 @@
17
17
 
18
18
  import { Log } from '../lib/log.js'
19
19
  import speechSynthesis from './speechSynthesis.js'
20
+ import { platform } from '../platform.js'
20
21
 
21
22
  let active = false
22
23
  let count = 0
@@ -27,7 +28,7 @@ let debounce = null
27
28
 
28
29
  // Global default utterance options
29
30
  let globalDefaultOptions = {
30
- enableUtteranceKeepAlive: !/android/i.test((self.navigator || {}).userAgent || ''),
31
+ enableUtteranceKeepAlive: !/android/i.test(platform.userAgent || ''),
31
32
  }
32
33
 
33
34
  const noopAnnouncement = {
@@ -16,8 +16,7 @@
16
16
  */
17
17
 
18
18
  import { Log } from '../lib/log.js'
19
-
20
- const syn = self.speechSynthesis
19
+ import { platform } from '../platform.js'
21
20
 
22
21
  const utterances = new Map() // id -> { utterance, timer, ignoreResume }
23
22
 
@@ -36,6 +35,7 @@ const clear = (id) => {
36
35
  }
37
36
 
38
37
  const startKeepAlive = (id) => {
38
+ const syn = platform.speechSynthesis
39
39
  const state = utterances.get(id)
40
40
 
41
41
  // utterance status: utterance was removed (cancelled or finished)
@@ -85,6 +85,7 @@ const defaultUtteranceProps = {
85
85
  }
86
86
 
87
87
  const initialize = () => {
88
+ const syn = platform.speechSynthesis
88
89
  // syn api check: syn might not have getVoices method
89
90
  if (!syn || typeof syn.getVoices !== 'function') {
90
91
  initialized = false
@@ -98,6 +99,7 @@ const initialize = () => {
98
99
 
99
100
  const waitForSynthReady = (timeoutMs = 2000, checkIntervalMs = 100) => {
100
101
  return new Promise((resolve) => {
102
+ const syn = platform.speechSynthesis
101
103
  if (!syn) {
102
104
  Log.debug('SpeechSynthesis - syn unavailable')
103
105
  resolve()
@@ -135,6 +137,7 @@ const waitForSynthReady = (timeoutMs = 2000, checkIntervalMs = 100) => {
135
137
  }
136
138
 
137
139
  const speak = async (options) => {
140
+ const syn = platform.speechSynthesis
138
141
  // options check: missing required options
139
142
  if (!options || !options.message) {
140
143
  return Promise.reject({ error: 'Missing message' })
@@ -154,6 +157,11 @@ const speak = async (options) => {
154
157
  // Wait for engine to be ready
155
158
  await waitForSynthReady()
156
159
 
160
+ const SpeechSynthesisUtterance = platform.SpeechSynthesisUtterance
161
+ if (SpeechSynthesisUtterance === undefined) {
162
+ return Promise.reject({ error: 'unavailable' })
163
+ }
164
+
157
165
  const utterance = new SpeechSynthesisUtterance(options.message)
158
166
  utterance.lang = options.lang || defaultUtteranceProps.lang
159
167
  utterance.pitch = options.pitch || defaultUtteranceProps.pitch
@@ -208,6 +216,7 @@ const speak = async (options) => {
208
216
 
209
217
  export default {
210
218
  speak(options) {
219
+ const syn = platform.speechSynthesis
211
220
  if (syn !== undefined) {
212
221
  if (initialized === false) {
213
222
  initialize()
@@ -219,6 +228,7 @@ export default {
219
228
  }
220
229
  },
221
230
  cancel() {
231
+ const syn = platform.speechSynthesis
222
232
  if (syn !== undefined) {
223
233
  // timers: clear all timers before cancelling
224
234
  for (const id of utterances.keys()) {
@@ -23,6 +23,7 @@ import Settings from './settings.js'
23
23
  import symbols from './lib/symbols.js'
24
24
  import { DEFAULT_HOLD_TIMEOUT_MS, DEFAULT_KEYMAP } from './constants.js'
25
25
  import { renderer } from './launch.js'
26
+ import { platform } from './platform.js'
26
27
 
27
28
  /**
28
29
  * Merged keyMap (default + custom settings).
@@ -31,6 +32,10 @@ import { renderer } from './launch.js'
31
32
  */
32
33
  export let keyMap = {}
33
34
 
35
+ export const initKeyMap = () => {
36
+ keyMap = { ...DEFAULT_KEYMAP, ...Settings.get('keymap', {}) }
37
+ }
38
+
34
39
  const Application = (config) => {
35
40
  config.hooks = config.hooks || {}
36
41
 
@@ -39,22 +44,23 @@ const Application = (config) => {
39
44
  let mouseMoveHandler
40
45
  let mouseClickHandler
41
46
  let updateCanvasRect
47
+ let inputCleanup
48
+ let mouseCleanup
42
49
  let holdTimeout
43
50
  let lastInputTime = 0
44
51
  let lastInputKey = null
45
- let mouseListenersAdded = false
46
52
 
47
53
  config.hooks[symbols.destroy] = function () {
48
54
  // Cancel pending key-hold timeout and reset hold state so focus is not left in hold mode after teardown
49
55
  clearTimeout(holdTimeout)
50
56
  Focus.hold = false
51
- document.removeEventListener('keydown', keyDownHandler)
52
- document.removeEventListener('keyup', keyUpHandler)
53
- if (mouseListenersAdded) {
54
- document.removeEventListener('mousemove', mouseMoveHandler)
55
- document.removeEventListener('click', mouseClickHandler)
56
- window.removeEventListener('resize', updateCanvasRect)
57
- window.removeEventListener('scroll', updateCanvasRect)
57
+ if (typeof inputCleanup === 'function') {
58
+ inputCleanup()
59
+ inputCleanup = undefined
60
+ }
61
+ if (typeof mouseCleanup === 'function') {
62
+ mouseCleanup()
63
+ mouseCleanup = undefined
58
64
  Hover.clear()
59
65
  }
60
66
  }
@@ -68,15 +74,16 @@ const Application = (config) => {
68
74
  this.$announcer.configure(announcerOptions)
69
75
  }
70
76
  // Initialize merged keyMap once during application init
71
- keyMap = { ...DEFAULT_KEYMAP, ...Settings.get('keymap', {}) }
77
+ initKeyMap()
72
78
 
73
79
  /** @type {number} Input throttle time in milliseconds (0 = disabled) */
74
80
  const throttleMs = Settings.get('inputThrottle', 0)
75
81
 
76
82
  const mouseEnabled = Settings.get('enableMouse', false)
83
+ const { createKeyboardEvent, input, isKeyboardEvent, now = Date.now, viewport } = platform
77
84
 
78
85
  keyDownHandler = async (e) => {
79
- const currentTime = performance.now()
86
+ const currentTime = now()
80
87
 
81
88
  const key = keyMap[e.keyCode] || e.keyCode
82
89
  const sameKey = lastInputKey === key
@@ -103,7 +110,7 @@ const Application = (config) => {
103
110
  ) {
104
111
  e = await this[symbols.inputEvents].intercept.call(this, e)
105
112
  // only pass on the key press to focused component when keyboard event is returned
106
- if (e instanceof KeyboardEvent === false) return
113
+ if (isKeyboardEvent && isKeyboardEvent(e) === false) return
107
114
  }
108
115
 
109
116
  Focus.input(key, e)
@@ -183,7 +190,9 @@ const Application = (config) => {
183
190
  mouseClickHandler = () => {
184
191
  if (currentComponent === undefined || currentComponent.eol === true) return
185
192
 
186
- const e = new KeyboardEvent('keydown', {
193
+ if (createKeyboardEvent === undefined) return
194
+
195
+ const e = createKeyboardEvent('keydown', {
187
196
  key: 'Enter',
188
197
  code: 'Enter',
189
198
  keyCode: 13,
@@ -195,15 +204,26 @@ const Application = (config) => {
195
204
  currentComponent.$input(e)
196
205
  }
197
206
 
198
- document.addEventListener('keydown', keyDownHandler)
199
- document.addEventListener('keyup', keyUpHandler)
200
- if (mouseEnabled === true) {
207
+ if (input !== undefined) {
208
+ input.addEventListener('keydown', keyDownHandler)
209
+ input.addEventListener('keyup', keyUpHandler)
210
+ inputCleanup = () => {
211
+ input.removeEventListener('keydown', keyDownHandler)
212
+ input.removeEventListener('keyup', keyUpHandler)
213
+ }
214
+ }
215
+ if (mouseEnabled === true && input !== undefined && viewport !== undefined) {
201
216
  updateCanvasRect()
202
- document.addEventListener('mousemove', mouseMoveHandler)
203
- document.addEventListener('click', mouseClickHandler)
204
- window.addEventListener('resize', updateCanvasRect)
205
- window.addEventListener('scroll', updateCanvasRect)
206
- mouseListenersAdded = true
217
+ input.addEventListener('mousemove', mouseMoveHandler)
218
+ input.addEventListener('click', mouseClickHandler)
219
+ viewport.addEventListener('resize', updateCanvasRect)
220
+ viewport.addEventListener('scroll', updateCanvasRect)
221
+ mouseCleanup = () => {
222
+ input.removeEventListener('mousemove', mouseMoveHandler)
223
+ input.removeEventListener('click', mouseClickHandler)
224
+ viewport.removeEventListener('resize', updateCanvasRect)
225
+ viewport.removeEventListener('scroll', updateCanvasRect)
226
+ }
207
227
  }
208
228
 
209
229
  // next tick
@@ -23,6 +23,7 @@ import { Log } from '../../lib/log.js'
23
23
  import { removeGlobalEffects } from '../../lib/reactivity/effect.js'
24
24
  import { renderer } from '../../launch.js'
25
25
  import { keyMap } from '../../application.js'
26
+ import { platform } from '../../platform.js'
26
27
 
27
28
  export default {
28
29
  $focus: {
@@ -48,7 +49,11 @@ export default {
48
49
  * @returns {boolean} - Returns true if this component or a parent component handled the event, false otherwise
49
50
  */
50
51
  value: function (event) {
51
- if (event === null || event === undefined || event instanceof KeyboardEvent === false)
52
+ if (
53
+ event === null ||
54
+ event === undefined ||
55
+ (platform.isKeyboardEvent && platform.isKeyboardEvent(event) === false)
56
+ )
52
57
  return false
53
58
 
54
59
  const key = keyMap[event.keyCode] || event.keyCode
package/src/component.js CHANGED
@@ -92,10 +92,14 @@ export const componentMap = new WeakMap()
92
92
  * @typedef {Object} BlitsElement
93
93
  * @property {BlitsComponent} component - Reference to the owning Blits component.
94
94
  * @property {BlitsElementConfig} config - Configuration object for the element.
95
+ * @property {boolean} eol - Indicates when the element is end-of-life (destroyed).
95
96
  * @property {number} counter - Unique counter used for shader workarounds. FIXME?
96
97
  * @property {string[]} effectNames - Names of active shader effects.
97
98
  * @property {any} node - The underlying renderer node (e.g., WebGL node or text node).
98
99
  * @property {BlitsElementProps} props - Proxy-like object containing transformed props.
100
+ * @property {Object<string, any>} scheduledTransitions - Tracks transitions by property name.
101
+ * @property {Object<string, ReturnType<typeof setTimeout>>} debounceTimeouts - Pending animation debounce timers.
102
+ * @property {any} [_spriteState] - Native Sprite texture cache (L3 only).
99
103
  * @property {any[]} children - WVB I cant see this populated? Filtered list of children owned by this element. FIXME?
100
104
  * @property {any} parent - WVB Shortcut to the parent CoreNode?
101
105
  * @property {number} nodeId - ID of the CoreNode, if available.
@@ -105,6 +109,9 @@ export const componentMap = new WeakMap()
105
109
  * @property {function(string, any, Object):void} animate - Animates a property with transition options.
106
110
  * @property {function():void} destroy - Destroys the underlying node and cancels transitions.
107
111
  * @property {function(any): any} triggerLayout - Triggers a layout update for the element.
112
+ * @property {function(Object):void} setInspectorMetadata - Merges inspector metadata onto the node.
113
+ * @property {function():void} _scheduleNativeSpriteSync - Queues a coalesced native Sprite texture sync.
114
+ * @property {function():void} _syncNativeSprite - Resolves and applies the native Sprite texture.
108
115
  *
109
116
  * @typedef {Object} BlitsLifecycle
110
117
  * @property {BlitsComponent} component - The Blits comonent instance this lifecycle belongs to.
@@ -35,6 +35,9 @@ export default () =>
35
35
  'color',
36
36
  'src',
37
37
  'texture',
38
+ 'image',
39
+ 'map',
40
+ 'frame',
38
41
  'fit',
39
42
  'rtt',
40
43
  'mount',