@lightningjs/blits 2.5.0 → 2.7.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.7.0
4
+
5
+ _2 jul 2026_
6
+
7
+ - Added `keepAlive` option to `src`-object to prevent a texture from being cleaned up by the renderer
8
+ - Abstracted announcer (and speechSynthesis) to be platform provided (with speechSynthesis as the built-in default)
9
+ - Added dedicated findByData methods to testing harness to select specific nodes in the tree
10
+ - Improved test cases around frameTick cleanup
11
+
12
+ ## 2.6.0
13
+
14
+ _25 jun 2026_
15
+
16
+ - Added platform layer for browser API abstraction (passed as a launch option)
17
+ - Improved reactivity guard vite plugin
18
+ - Added testing harness to support Blits component testing
19
+
3
20
  ## 2.5.0
4
21
 
5
22
  _11 jun 2026_
package/index.d.ts CHANGED
@@ -66,6 +66,28 @@ declare module '@lightningjs/blits' {
66
66
  enableUtteranceKeepAlive?: boolean
67
67
  }
68
68
 
69
+ export interface AnnouncerDriverOptions extends AnnouncerUtteranceOptions {
70
+ /**
71
+ * Message to be spoken by the platform announcer driver.
72
+ */
73
+ message: string | number,
74
+ /**
75
+ * Internal announcement id used by the Blits announcer queue.
76
+ */
77
+ id: number,
78
+ }
79
+
80
+ export interface AnnouncerDriver {
81
+ /**
82
+ * Speak one queued announcement.
83
+ */
84
+ speak(options: AnnouncerDriverOptions): Promise<any>,
85
+ /**
86
+ * Stop the active platform announcement, when supported.
87
+ */
88
+ cancel(): void,
89
+ }
90
+
69
91
  export interface AnnouncerUtterance<T = any> extends Promise<T> {
70
92
  /**
71
93
  * Removes a specific message from the announcement queue,
@@ -1016,6 +1038,70 @@ declare module '@lightningjs/blits' {
1016
1038
  type ReactivityModes = 'Proxy' | 'defineProperty'
1017
1039
  type RenderModes = 'webgl' | 'canvas'
1018
1040
 
1041
+ interface Platform {
1042
+ /**
1043
+ * Target used for keyboard and pointer input listeners.
1044
+ */
1045
+ input?: EventTarget | any,
1046
+ /**
1047
+ * Target used for viewport-level listeners such as resize, scroll, and hashchange.
1048
+ */
1049
+ viewport?: EventTarget | any,
1050
+ /**
1051
+ * Dispatch an event through the platform's event system.
1052
+ */
1053
+ dispatchEvent?: (event: any) => boolean | void,
1054
+ /**
1055
+ * Persistent key/value storage API.
1056
+ */
1057
+ localStorage?: Storage | any,
1058
+ /**
1059
+ * Read the platform cookie string.
1060
+ */
1061
+ getCookie?: () => string,
1062
+ /**
1063
+ * Write a cookie string.
1064
+ */
1065
+ setCookie?: (value: string) => void,
1066
+ /**
1067
+ * Navigate back in the platform history, when available.
1068
+ */
1069
+ historyBack?: () => void,
1070
+ /**
1071
+ * Current viewport/screen height.
1072
+ */
1073
+ screenHeight?: number,
1074
+ /**
1075
+ * Available hardware concurrency.
1076
+ */
1077
+ hardwareConcurrency?: number,
1078
+ /**
1079
+ * User agent string for platform-specific defaults.
1080
+ */
1081
+ userAgent?: string,
1082
+ /**
1083
+ * KeyboardEvent constructor used for event creation and type checks.
1084
+ */
1085
+ KeyboardEvent?: typeof KeyboardEvent | any,
1086
+ /**
1087
+ * Check whether an object should be treated as a keyboard event.
1088
+ */
1089
+ isKeyboardEvent?: (event: any) => boolean,
1090
+ /**
1091
+ * Create a keyboard event for the current platform.
1092
+ */
1093
+ createKeyboardEvent?: (type: string, init?: KeyboardEventInit | any) => any,
1094
+ /**
1095
+ * Platform-specific announcer driver used instead of the default Web Speech driver.
1096
+ */
1097
+ announcer?: AnnouncerDriver,
1098
+ /**
1099
+ * Monotonic timestamp provider used for input throttling.
1100
+ */
1101
+ now?: () => number,
1102
+ [key: string]: any,
1103
+ }
1104
+
1019
1105
  /**
1020
1106
  * Settings
1021
1107
  *
@@ -1331,11 +1417,20 @@ declare module '@lightningjs/blits' {
1331
1417
  */
1332
1418
  maxFPS?: number,
1333
1419
  /**
1334
- * Custom platform layer to be used by the App (experimental)
1420
+ * Custom platform layer to be used by the App.
1421
+ *
1422
+ * Allows replacing platform capabilities such as input targets, storage,
1423
+ * keyboard event constructors, and speech APIs.
1424
+ * A callback receives Blits' browser defaults and should return the platform overrides.
1425
+ */
1426
+ platform?: (defaults: Platform) => Partial<Platform>,
1427
+ /**
1428
+ * Custom platform layer to be used by the renderer.
1335
1429
  *
1336
- * Defaults to `null` which will be resolved internally to WebPlatform
1430
+ * For renderer-specific platform implementations. Passing a renderer platform
1431
+ * through `platform` is deprecated.
1337
1432
  */
1338
- platform?: any
1433
+ rendererPlatform?: RendererMainSettings['platform']
1339
1434
 
1340
1435
  }
1341
1436
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "2.5.0",
3
+ "version": "2.7.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -32,6 +32,14 @@
32
32
  "types": "./src/plugins/storage/storage.d.ts",
33
33
  "default": "./src/plugins/storage/storage.js"
34
34
  },
35
+ "./platforms/webOS/announcer": {
36
+ "types": "./src/platforms/webOS/announcer.d.ts",
37
+ "default": "./src/platforms/webOS/announcer.js"
38
+ },
39
+ "./testing": {
40
+ "types": "./src/testing/index.d.ts",
41
+ "default": "./src/testing/index.js"
42
+ },
35
43
  "./symbols": "./src/lib/symbols.js",
36
44
  "./blitsFileConverter": "./src/lib/blitsfileconverter/blitsfileconverter.js",
37
45
  "./components": "./src/components/FPScounter.js"
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import { Log } from '../lib/log.js'
19
- import speechSynthesis from './speechSynthesis.js'
19
+ import { platform } from '../platform.js'
20
20
 
21
21
  let active = false
22
22
  let count = 0
@@ -27,7 +27,21 @@ let debounce = null
27
27
 
28
28
  // Global default utterance options
29
29
  let globalDefaultOptions = {
30
- enableUtteranceKeepAlive: !/android/i.test((self.navigator || {}).userAgent || ''),
30
+ enableUtteranceKeepAlive: !/android/i.test(platform.userAgent || ''),
31
+ }
32
+
33
+ const getDriver = () => {
34
+ const driver = platform.announcer
35
+ if (driver && typeof driver.speak === 'function') {
36
+ return driver
37
+ }
38
+ }
39
+
40
+ const cancelDriver = () => {
41
+ const driver = getDriver()
42
+ if (driver && typeof driver.cancel === 'function') {
43
+ driver.cancel()
44
+ }
31
45
  }
32
46
 
33
47
  const noopAnnouncement = {
@@ -83,9 +97,9 @@ const addToQueue = (message, politeness, delay = false, options = {}) => {
83
97
  // augment the promise with a stop function
84
98
  done.stop = () => {
85
99
  if (id === currentId) {
86
- speechSynthesis.cancel()
100
+ cancelDriver()
87
101
  isProcessing = false
88
- resolveFn('interupted')
102
+ resolveFn('interrupted')
89
103
  }
90
104
  }
91
105
 
@@ -132,7 +146,17 @@ const processQueue = async () => {
132
146
  debounce = setTimeout(() => {
133
147
  Log.debug(`Announcer - speaking: "${message}" (id: ${id})`)
134
148
 
135
- speechSynthesis
149
+ const driver = getDriver()
150
+ if (driver === undefined) {
151
+ currentId = null
152
+ currentResolveFn = null
153
+ isProcessing = false
154
+ resolveFn('unavailable')
155
+ processQueue()
156
+ return
157
+ }
158
+
159
+ driver
136
160
  .speak({
137
161
  message,
138
162
  id,
@@ -179,8 +203,8 @@ const stop = () => {
179
203
  // Clear debounce timer if speech hasn't started yet
180
204
  clearDebounceTimer()
181
205
 
182
- // Always cancel speech synthesis to ensure clean state
183
- speechSynthesis.cancel()
206
+ // Always cancel the platform announcer to ensure clean state
207
+ cancelDriver()
184
208
 
185
209
  // Store resolve function before resetting state
186
210
  const resolveFn = currentResolveFn
@@ -17,11 +17,27 @@
17
17
 
18
18
  import { Log } from '../lib/log.js'
19
19
 
20
- const syn = self.speechSynthesis
21
-
22
20
  const utterances = new Map() // id -> { utterance, timer, ignoreResume }
23
21
 
24
22
  let initialized = false
23
+ const globalScope = globalThis
24
+
25
+ const getSpeechSynthesis = () => {
26
+ const windowRef = globalScope.window
27
+ const selfRef = globalScope.self || windowRef || globalScope
28
+
29
+ return (
30
+ globalScope.speechSynthesis ||
31
+ (selfRef && selfRef.speechSynthesis) ||
32
+ (windowRef && windowRef.speechSynthesis)
33
+ )
34
+ }
35
+
36
+ const getSpeechSynthesisUtterance = () => {
37
+ const windowRef = globalScope.window
38
+
39
+ return globalScope.SpeechSynthesisUtterance || (windowRef && windowRef.SpeechSynthesisUtterance)
40
+ }
25
41
 
26
42
  const clear = (id) => {
27
43
  const state = utterances.get(id)
@@ -36,6 +52,7 @@ const clear = (id) => {
36
52
  }
37
53
 
38
54
  const startKeepAlive = (id) => {
55
+ const syn = getSpeechSynthesis()
39
56
  const state = utterances.get(id)
40
57
 
41
58
  // utterance status: utterance was removed (cancelled or finished)
@@ -85,6 +102,7 @@ const defaultUtteranceProps = {
85
102
  }
86
103
 
87
104
  const initialize = () => {
105
+ const syn = getSpeechSynthesis()
88
106
  // syn api check: syn might not have getVoices method
89
107
  if (!syn || typeof syn.getVoices !== 'function') {
90
108
  initialized = false
@@ -98,6 +116,7 @@ const initialize = () => {
98
116
 
99
117
  const waitForSynthReady = (timeoutMs = 2000, checkIntervalMs = 100) => {
100
118
  return new Promise((resolve) => {
119
+ const syn = getSpeechSynthesis()
101
120
  if (!syn) {
102
121
  Log.debug('SpeechSynthesis - syn unavailable')
103
122
  resolve()
@@ -135,6 +154,7 @@ const waitForSynthReady = (timeoutMs = 2000, checkIntervalMs = 100) => {
135
154
  }
136
155
 
137
156
  const speak = async (options) => {
157
+ const syn = getSpeechSynthesis()
138
158
  // options check: missing required options
139
159
  if (!options || !options.message) {
140
160
  return Promise.reject({ error: 'Missing message' })
@@ -154,6 +174,11 @@ const speak = async (options) => {
154
174
  // Wait for engine to be ready
155
175
  await waitForSynthReady()
156
176
 
177
+ const SpeechSynthesisUtterance = getSpeechSynthesisUtterance()
178
+ if (SpeechSynthesisUtterance === undefined) {
179
+ return Promise.reject({ error: 'unavailable' })
180
+ }
181
+
157
182
  const utterance = new SpeechSynthesisUtterance(options.message)
158
183
  utterance.lang = options.lang || defaultUtteranceProps.lang
159
184
  utterance.pitch = options.pitch || defaultUtteranceProps.pitch
@@ -208,6 +233,7 @@ const speak = async (options) => {
208
233
 
209
234
  export default {
210
235
  speak(options) {
236
+ const syn = getSpeechSynthesis()
211
237
  if (syn !== undefined) {
212
238
  if (initialized === false) {
213
239
  initialize()
@@ -219,6 +245,7 @@ export default {
219
245
  }
220
246
  },
221
247
  cancel() {
248
+ const syn = getSpeechSynthesis()
222
249
  if (syn !== undefined) {
223
250
  // timers: clear all timers before cancelling
224
251
  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
@@ -38,6 +38,11 @@ import { plugins } from './plugin.js'
38
38
 
39
39
  // object to store global components
40
40
  let globalComponents
41
+ let devMode = false
42
+
43
+ export const setDevMode = (enabled) => {
44
+ devMode = enabled === true
45
+ }
41
46
 
42
47
  const required = (name) => {
43
48
  throw new Error(`Parameter ${name} is required`)
@@ -493,7 +498,7 @@ const Component = (name = required('name'), config = required('config')) => {
493
498
  // one time code generation (only if precompilation is turned off)
494
499
  if (config.code === undefined) {
495
500
  Log.debug(`Generating code for ${name} component`)
496
- config.code = codegenerator.call(config, parser(config.template, name))
501
+ config.code = codegenerator.call(config, parser(config.template, name), devMode)
497
502
  }
498
503
 
499
504
  // create an instance of the component, using base as the prototype (which contains Base)
@@ -23,6 +23,7 @@ import Router, {
23
23
  } from '../router/router.js'
24
24
  import symbols from '../lib/symbols.js'
25
25
  import Focus from '../focus/focus.js'
26
+ import { platform } from '../platform.js'
26
27
 
27
28
  /** @typedef {{ $input?: (event: any) => boolean, $focus?: (event: any) => void }} RouterViewParent */
28
29
 
@@ -159,11 +160,18 @@ export default () =>
159
160
  Router.navigate.apply(this)
160
161
  }
161
162
 
162
- window.addEventListener('hashchange', this.hashchangeHandler)
163
+ const viewport = platform.viewport
164
+ if (viewport !== undefined) {
165
+ this.hashchangeViewport = viewport
166
+ viewport.addEventListener('hashchange', this.hashchangeHandler)
167
+ }
163
168
  },
164
169
  destroy() {
165
170
  unregisterRouterView(this)
166
- window.removeEventListener('hashchange', this.hashchangeHandler, false)
171
+ if (this.hashchangeViewport !== undefined) {
172
+ this.hashchangeViewport.removeEventListener('hashchange', this.hashchangeHandler, false)
173
+ this.hashchangeViewport = undefined
174
+ }
167
175
  },
168
176
  focus() {
169
177
  if (this.activeView && Focus.get() === this) {
@@ -202,6 +202,15 @@ const colorMap = {
202
202
  right: 'colorRight',
203
203
  }
204
204
 
205
+ const setTextureOption = (target, key, value) => {
206
+ const opts =
207
+ target.props['textureOptions'] ||
208
+ (target.element.node && target.element.node.textureOptions) ||
209
+ {}
210
+ opts[key] = value
211
+ target.props['textureOptions'] = opts
212
+ }
213
+
205
214
  /**
206
215
  * Default text settings for text nodes (initialized on first use).
207
216
  * @type {object|null}
@@ -275,6 +284,9 @@ const propsTransformer = {
275
284
  if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
276
285
  this.props['src'] = v.src
277
286
  this.props['imageType'] = v.type
287
+ if ('keepAlive' in v) {
288
+ setTextureOption(this, 'preventCleanup', v.keepAlive === true)
289
+ }
278
290
  } else {
279
291
  this.props['src'] = v
280
292
  }
@@ -318,7 +330,7 @@ const propsTransformer = {
318
330
  const resizeMode = {}
319
331
 
320
332
  if (v === 'cover' || v === 'contain') {
321
- this.props['textureOptions'] = { resizeMode: { type: v } }
333
+ setTextureOption(this, 'resizeMode', { type: v })
322
334
  return
323
335
  }
324
336
 
@@ -333,7 +345,7 @@ const propsTransformer = {
333
345
  resizeMode['clipX'] = 'x' in v.position === true ? v.position.x : null
334
346
  resizeMode['clipY'] = 'y' in v.position === true ? v.position.y : null
335
347
  }
336
- this.props['textureOptions'] = { resizeMode }
348
+ setTextureOption(this, 'resizeMode', resizeMode)
337
349
  }
338
350
  },
339
351
  set rtt(v) {
@@ -25,6 +25,7 @@ import { SCREEN_RESOLUTIONS, RENDER_QUALITIES } from '../../constants.js'
25
25
  import colors from '../../lib/colors/colors.js'
26
26
  import fontLoader from './fontLoader.js'
27
27
  import shaderLoader from './shaderLoader.js'
28
+ import { platform } from '../../platform.js'
28
29
 
29
30
  /** @type {RendererMain|{}} */
30
31
  export let renderer = {}
@@ -73,6 +74,11 @@ const textureMemorySettings = (settings) => {
73
74
  *
74
75
  */
75
76
  export default (App, target, settings = {}) => {
77
+ const screenHeight = settings.platform ? settings.platform.screenHeight : platform.screenHeight
78
+ const hardwareConcurrency = settings.platform
79
+ ? settings.platform.hardwareConcurrency
80
+ : platform.hardwareConcurrency
81
+
76
82
  renderer = new RendererMain(
77
83
  {
78
84
  ...{
@@ -84,12 +90,10 @@ export default (App, target, settings = {}) => {
84
90
  deviceLogicalPixelRatio:
85
91
  settings.pixelRatio ||
86
92
  SCREEN_RESOLUTIONS[settings.screenResolution] ||
87
- SCREEN_RESOLUTIONS[window.innerHeight] ||
93
+ SCREEN_RESOLUTIONS[screenHeight] ||
88
94
  1,
89
95
  numImageWorkers:
90
- 'webWorkersLimit' in settings
91
- ? settings.webWorkersLimit
92
- : window.navigator.hardwareConcurrency || 2,
96
+ 'webWorkersLimit' in settings ? settings.webWorkersLimit : hardwareConcurrency || 2,
93
97
  clearColor: (settings.canvasColor && colors.normalize(settings.canvasColor)) || 0x00000000,
94
98
  inspector: settings.inspector === true ? Inspector : undefined,
95
99
  boundsMargin: settings.viewportMargin || 0,
@@ -100,7 +104,7 @@ export default (App, target, settings = {}) => {
100
104
  textureMemory: textureMemorySettings(settings),
101
105
  createImageBitmapSupport: 'auto',
102
106
  targetFPS: 'maxFPS' in settings ? settings.maxFPS : 0,
103
- platform: 'platform' in settings ? settings.platform : null,
107
+ platform: settings.rendererPlatform || null,
104
108
  },
105
109
  ...(settings.advanced || {}),
106
110
  },
@@ -21,7 +21,7 @@ import Settings from '../settings.js'
21
21
  import { DEFAULT_HOLD_TIMEOUT_MS } from '../constants.js'
22
22
  import { Log } from '../lib/log.js'
23
23
  import { getAncestors } from './helpers.js'
24
- import InternalKeyboardEvent from '../lib/events/internalkeyboardevent.js'
24
+ import { platform } from '../platform.js'
25
25
 
26
26
  let focusedComponent = null
27
27
  let focusChain = []
@@ -136,8 +136,11 @@ const setFocus = (component, event) => {
136
136
  focusedComponent = component
137
137
  component[symbols.lifecycle].state = 'focus'
138
138
 
139
- if (event instanceof KeyboardEvent) {
140
- const internalEvent = new InternalKeyboardEvent('keydown', event)
141
- document.dispatchEvent(internalEvent)
139
+ if (platform.isKeyboardEvent && platform.isKeyboardEvent(event) && platform.dispatchEvent) {
140
+ const internalEvent = platform.createKeyboardEvent
141
+ ? platform.createKeyboardEvent('keydown', event)
142
+ : event
143
+ internalEvent[symbols.internalEvent] = true
144
+ platform.dispatchEvent(internalEvent)
142
145
  }
143
146
  }