@lightningjs/blits 2.0.0-beta.2 → 2.0.0-beta.4

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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.50.0
4
+
5
+ _27 feb 2026_
6
+
7
+ - Fixed issue with router navigation options not properly being overriden
8
+ - Fixed FPS counter component and made it into a separate importable component (`import { FPScounter } from @lightningjs/blits/components`)
9
+ - Added mouse (magic wand) support (including new `hover` lifecycle hook and `isHovered` state indicator)
10
+ - Updated template attributes for VScode extension
11
+
12
+
13
+ ## v1.49.0
14
+
15
+ _20 feb 2026_
16
+
17
+ - Added `data-blits-renderstate` inspector attribute
18
+ - Fixed `data-blits-hasfocus` inspector attribute to always be available
19
+ - Added boolean attribute to `idle` lifecycle hook to indicate `idle` or `active` state
20
+ - Fixed type definitions for router beforeEach hooks
21
+ - Fixed double processing of reactivity guard
3
22
 
4
23
  ## v1.48.0
5
24
 
package/index.d.ts CHANGED
@@ -174,6 +174,8 @@ declare module '@lightningjs/blits' {
174
174
  * Fires when the Component is being destroyed and removed.
175
175
  */
176
176
  destroy?: () => void;
177
+ hover?: () => void;
178
+ unhover?: () => void;
177
179
  /**
178
180
  * Fires upon each frame start (allowing you to tap directly into the renderloop)
179
181
  *
@@ -205,11 +207,14 @@ declare module '@lightningjs/blits' {
205
207
  */
206
208
  exit?: () => void;
207
209
  /**
208
- * Fires when the renderer is done rendering and enters an idle state
210
+ * Fires when the renderer is done rendering and enters an idle state (idle = true)
211
+ * and when the renderer starts rendering again and leaves the idle state (idle = false)
212
+ *
213
+ * @param idle - boolean to indicate whether the renderer is in idle state or not
209
214
  *
210
215
  * Note: This event can fire multiple times
211
216
  */
212
- idle?: () => void;
217
+ idle?: (idle: boolean) => void;
213
218
  /**
214
219
  * Fires at a predefined interval and reports the current FPS value
215
220
  *
@@ -395,6 +400,13 @@ declare module '@lightningjs/blits' {
395
400
  */
396
401
  readonly $hasFocus: boolean,
397
402
 
403
+ /**
404
+ * Indicates whether the component currently is hovered
405
+ *
406
+ * @returns Boolean
407
+ */
408
+ isHovered: boolean,
409
+
398
410
  /**
399
411
  * Listen to events emitted by other components
400
412
  */
@@ -653,9 +665,9 @@ declare module '@lightningjs/blits' {
653
665
 
654
666
  export interface RouterHooks {
655
667
  init?: () => Promise<void> | void;
656
- beforeEach?: (to: Route, from: Route) => string | Route | Promise<string | Route> | void;
657
- afterEach?: (to: Route, from: Route) => string | Route | Promise<string | Route> | void;
658
- error?: (err: string) => string | Route | Promise<string | Route> | void;
668
+ beforeEach?: (to: Route, from: Route) => string | Route | void | boolean | Promise<string | Route | void | boolean>;
669
+ afterEach?: (to: Route, from: Route) => string | Route | void | boolean | Promise<string | Route | void | boolean>;
670
+ error?: (err: string) => string | Route | void | boolean | Promise<string | Route | void | boolean>;
659
671
  }
660
672
 
661
673
  export interface RouterConfig<P extends Props, S, M, C> {
@@ -681,13 +693,13 @@ declare module '@lightningjs/blits' {
681
693
 
682
694
  /**
683
695
  * Enable or disable RouterView history navigation on Back input
684
- *
696
+ *
685
697
  * @default true
686
- *
698
+ *
687
699
  * @remarks
688
700
  * This is an app-wide setting that affects all RouterView instances in your application.
689
701
  * The router state is global and shared across all router instances.
690
- *
702
+ *
691
703
  * @example
692
704
  * ```js
693
705
  * router: {
@@ -796,8 +808,8 @@ declare module '@lightningjs/blits' {
796
808
  }[keyof T]
797
809
 
798
810
  export interface RouteHooks {
799
- before?: (to: Route, from: Route) => string | Route | Promise<string | Route>;
800
- after?: (to: Route, from: Route) => string | Route | Promise<string | Route>;
811
+ before?: (to: Route, from: Route) => string | Route | void | boolean | Promise<string | Route | void | boolean>;
812
+ after?: (to: Route, from: Route) => string | Route | void | boolean | Promise<string | Route | void | boolean>;
801
813
  }
802
814
 
803
815
  export type Route = {
@@ -1221,6 +1233,16 @@ declare module '@lightningjs/blits' {
1221
1233
  * ```
1222
1234
  */
1223
1235
  announcerOptions?: AnnouncerUtteranceOptions,
1236
+ /**
1237
+ * Enable mouse support (hover and click-to-focus).
1238
+ *
1239
+ * When set to `true`, pointer movement over the canvas updates hover state on components
1240
+ * and click dispatches focus and Enter key input to the component under the cursor.
1241
+ * When set to `false`, no mouse or pointer listeners are registered.
1242
+ *
1243
+ * @default false
1244
+ */
1245
+ enableMouse?: boolean,
1224
1246
  /**
1225
1247
  * Maximum FPS at which the App will be rendered
1226
1248
  *
@@ -1229,7 +1251,14 @@ declare module '@lightningjs/blits' {
1229
1251
  *
1230
1252
  * Defaults to `0` which means no maximum
1231
1253
  */
1232
- maxFPS?: number
1254
+ maxFPS?: number,
1255
+ /**
1256
+ * Custom platform layer to be used by the App (experimental)
1257
+ *
1258
+ * Defaults to `null` which will be resolved internally to WebPlatform
1259
+ */
1260
+ platform?: any
1261
+
1233
1262
  }
1234
1263
 
1235
1264
  interface State {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "2.0.0-beta.2",
3
+ "version": "2.0.0-beta.4",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -29,7 +29,8 @@
29
29
  "default": "./src/plugins/storage/storage.js"
30
30
  },
31
31
  "./symbols": "./src/lib/symbols.js",
32
- "./blitsFileConverter": "./src/lib/blitsfileconverter/blitsfileconverter.js"
32
+ "./blitsFileConverter": "./src/lib/blitsfileconverter/blitsfileconverter.js",
33
+ "./components": "./src/components/FPScounter.js"
33
34
  },
34
35
  "scripts": {
35
36
  "test": "c8 node scripts/runTests.js",
@@ -75,7 +76,7 @@
75
76
  "tape": "^5.5.0"
76
77
  },
77
78
  "dependencies": {
78
- "@lightningjs/renderer": "3.0.0-beta21",
79
+ "@lightningjs/renderer": "3.0.0-beta22",
79
80
  "magic-string": "^0.30.21"
80
81
  },
81
82
  "repository": {
@@ -19,7 +19,7 @@ import fs from 'fs'
19
19
  import path from 'path'
20
20
  import { fileURLToPath } from 'url'
21
21
  import compiler from '../src/lib/precompiler/precompiler.js'
22
- import { exec } from 'child_process'
22
+ import childProcess from 'child_process'
23
23
 
24
24
  const currentDir = process.cwd()
25
25
  const componentDirs = ['src/components']
@@ -71,7 +71,7 @@ function processFile(filePath) {
71
71
  function formatFileWithESLint(filePath) {
72
72
  const command = `eslint "${filePath}" --fix`
73
73
 
74
- exec(command, (error, stdout, stderr) => {
74
+ childProcess.exec(command, (error, stdout, stderr) => {
75
75
  if (error) {
76
76
  console.error(`Execution Error: ${error.message}`)
77
77
  console.error(`Exit Code: ${error.code}`)
@@ -15,12 +15,14 @@
15
15
  * SPDX-License-Identifier: Apache-2.0
16
16
  */
17
17
 
18
- import Component from './component.js'
19
- import { default as Focus, keyUpCallbacks } from './focus.js'
20
- import Settings from './settings.js'
18
+ import { default as Component, componentMap } from './component.js'
19
+ import { default as Focus, keyUpCallbacks } from './focus/focus.js'
20
+ import Hover from './focus/hover.js'
21
21
 
22
+ import Settings from './settings.js'
22
23
  import symbols from './lib/symbols.js'
23
24
  import { DEFAULT_HOLD_TIMEOUT_MS, DEFAULT_KEYMAP } from './constants.js'
25
+ import { renderer } from './launch.js'
24
26
 
25
27
  /**
26
28
  * Merged keyMap (default + custom settings).
@@ -34,13 +36,24 @@ const Application = (config) => {
34
36
 
35
37
  let keyDownHandler
36
38
  let keyUpHandler
39
+ let mouseMoveHandler
40
+ let mouseClickHandler
41
+ let updateCanvasRect
37
42
  let holdTimeout
38
43
  let lastInputTime = 0
39
44
  let lastInputKey = null
45
+ let mouseListenersAdded = false
40
46
 
41
47
  config.hooks[symbols.destroy] = function () {
42
48
  document.removeEventListener('keydown', keyDownHandler)
43
49
  document.removeEventListener('keyup', keyUpHandler)
50
+ if (mouseListenersAdded) {
51
+ document.removeEventListener('mousemove', mouseMoveHandler)
52
+ document.removeEventListener('click', mouseClickHandler)
53
+ window.removeEventListener('resize', updateCanvasRect)
54
+ window.removeEventListener('scroll', updateCanvasRect)
55
+ Hover.clear()
56
+ }
44
57
  }
45
58
 
46
59
  config.hooks[symbols.init] = function () {
@@ -57,6 +70,8 @@ const Application = (config) => {
57
70
  /** @type {number} Input throttle time in milliseconds (0 = disabled) */
58
71
  const throttleMs = Settings.get('inputThrottle', 0)
59
72
 
73
+ const mouseEnabled = Settings.get('enableMouse', false)
74
+
60
75
  keyDownHandler = async (e) => {
61
76
  const currentTime = performance.now()
62
77
 
@@ -89,6 +104,7 @@ const Application = (config) => {
89
104
  }
90
105
 
91
106
  Focus.input(key, e)
107
+ if (mouseEnabled === true) Hover.clear()
92
108
  clearTimeout(holdTimeout)
93
109
  holdTimeout = setTimeout(
94
110
  () => {
@@ -108,8 +124,84 @@ const Application = (config) => {
108
124
  Focus.hold = false
109
125
  }
110
126
 
127
+ let lastMoved = 0
128
+ let currentNode = undefined
129
+ let currentComponent = undefined
130
+ let canvasRect = null
131
+
132
+ updateCanvasRect = () => {
133
+ if (renderer.canvas !== undefined) {
134
+ canvasRect = renderer.canvas.getBoundingClientRect()
135
+ }
136
+ }
137
+
138
+ // limit the amount of move events per time frame
139
+ mouseMoveHandler = (e) => {
140
+ if (e.timeStamp - lastMoved < 100) return
141
+ lastMoved = e.timeStamp
142
+
143
+ this.$emit('mouse::move', e)
144
+
145
+ if (canvasRect === null && renderer.canvas != null) {
146
+ updateCanvasRect()
147
+ }
148
+ if (canvasRect === null) {
149
+ return
150
+ }
151
+
152
+ const stage = renderer.stage
153
+ if (stage == null) {
154
+ return
155
+ }
156
+
157
+ // Convert viewport → canvas display coordinates
158
+ const x = e.clientX - canvasRect.left
159
+ const y = e.clientY - canvasRect.top
160
+
161
+ const node = stage.getNodeFromPosition({ x, y })
162
+
163
+ if (node === null) {
164
+ currentComponent = undefined
165
+ Hover.clear()
166
+ return
167
+ }
168
+ if (node === currentNode) return
169
+
170
+ currentNode = node
171
+ currentComponent = componentMap.get(currentNode)
172
+
173
+ if (currentComponent === undefined) {
174
+ Hover.clear()
175
+ return
176
+ }
177
+ Hover.set(currentComponent)
178
+ }
179
+
180
+ mouseClickHandler = () => {
181
+ if (currentComponent === undefined || currentComponent.eol === true) return
182
+
183
+ const e = new KeyboardEvent('keydown', {
184
+ key: 'Enter',
185
+ code: 'Enter',
186
+ keyCode: 13,
187
+ bubbles: true,
188
+ cancelable: true,
189
+ composed: true,
190
+ })
191
+ currentComponent.$focus()
192
+ currentComponent.$input(e)
193
+ }
194
+
111
195
  document.addEventListener('keydown', keyDownHandler)
112
196
  document.addEventListener('keyup', keyUpHandler)
197
+ if (mouseEnabled === true) {
198
+ updateCanvasRect()
199
+ document.addEventListener('mousemove', mouseMoveHandler)
200
+ document.addEventListener('click', mouseClickHandler)
201
+ window.addEventListener('resize', updateCanvasRect)
202
+ window.addEventListener('scroll', updateCanvasRect)
203
+ mouseListenersAdded = true
204
+ }
113
205
 
114
206
  // next tick
115
207
  setTimeout(() => Focus.set(this))
@@ -16,8 +16,7 @@
16
16
  */
17
17
 
18
18
  import symbols from '../../lib/symbols.js'
19
- import Focus from '../../focus.js'
20
- import { keyUpCallbacks, getComponentWithInputEvent } from '../../focus.js'
19
+ import { default as Focus, keyUpCallbacks, getComponentWithInputEvent } from '../../focus/focus.js'
21
20
  import eventListeners from '../../lib/eventListeners.js'
22
21
  import { trigger } from '../../lib/reactivity/effect.js'
23
22
  import { Log } from '../../lib/log.js'
@@ -36,6 +36,8 @@ export default (component, state = () => {}) => {
36
36
  // add built-in $hasFocus key
37
37
  stateKeys.push('$hasFocus')
38
38
  }
39
+ // add built-in isHovered key
40
+ stateKeys.push(['isHovered'])
39
41
  const stateKeysLength = stateKeys.length
40
42
 
41
43
  for (let i = 0; i < stateKeysLength; i++) {
package/src/component.js CHANGED
@@ -43,6 +43,9 @@ const required = (name) => {
43
43
  throw new Error(`Parameter ${name} is required`)
44
44
  }
45
45
 
46
+ // map that holds component to their holder node
47
+ export const componentMap = new WeakMap()
48
+
46
49
  /**
47
50
  * @typedef {function} BlitsComponentFactory
48
51
  * @param {object} opts - The options for the component instance
@@ -217,6 +220,10 @@ const Component = (name = required('name'), config = required('config')) => {
217
220
  // such as rotation and scale to components)
218
221
  this[symbols.holder] = parentEl
219
222
 
223
+ if (parentEl !== undefined && parentEl.node !== undefined) {
224
+ componentMap.set(parentEl.node, this)
225
+ }
226
+
220
227
  // generate an internal id (simple counter)
221
228
  this[symbols.id] = createInternalId()
222
229
 
@@ -239,6 +246,9 @@ const Component = (name = required('name'), config = required('config')) => {
239
246
  // add $hasFocus key in
240
247
  this[symbols.originalState]['$hasFocus'] = false
241
248
 
249
+ // add isHovered key in
250
+ this[symbols.originalState]['isHovered'] = false
251
+
242
252
  // generate a reactive state (using the result of previously execute state function)
243
253
  // and store it
244
254
  this[symbols.state] = reactive(this[symbols.originalState], Settings.get('reactivityMode'))
@@ -268,35 +278,59 @@ const Component = (name = required('name'), config = required('config')) => {
268
278
  this[symbols.slots] = this[symbols.children].filter((child) => child[symbols.isSlot])
269
279
 
270
280
  this[symbols.rendererEventListeners] = []
281
+
282
+ if (this[symbols.holder] !== undefined && Settings.get('inspector', false)) {
283
+ this[symbols.holder].setInspectorMetadata({ 'blits-hasFocus': false })
284
+ }
285
+
286
+ // setup inspector data for render state
287
+ if (name === 'App' && Settings.get('inspector', false)) {
288
+ this.$setTimeout(() => {
289
+ const wrapperEl = this[symbols.wrapper]
290
+ const idleCb = () => {
291
+ wrapperEl.setInspectorMetadata({ 'blits-renderState': 'idle' })
292
+ }
293
+ const activeCb = () => {
294
+ wrapperEl.setInspectorMetadata({ 'blits-renderState': 'active' })
295
+ }
296
+ renderer.on('idle', idleCb)
297
+ renderer.on('active', activeCb)
298
+ this[symbols.rendererEventListeners].push({ event: 'idle', cb: idleCb })
299
+ this[symbols.rendererEventListeners].push({ event: 'active', cb: activeCb })
300
+ })
301
+ }
302
+
271
303
  // register hooks if component has hooks specified
272
304
  if (config.hooks) {
273
- // push to next tick to ensure
274
- setTimeout(() => {
275
- // frame tick event
276
- if (config.hooks.frameTick) {
277
- const cb = (r, data) => emit('frameTick', this[symbols.identifier], this, [data])
278
- this[symbols.rendererEventListeners].push({ event: 'frameTick', cb })
279
- renderer.on('frameTick', cb)
280
- }
305
+ // frame tick event
306
+ if (config.hooks.frameTick) {
307
+ const cb = (_r, data) => emit('frameTick', this[symbols.identifier], this, [data])
308
+ renderer.on('frameTick', cb)
309
+ this[symbols.rendererEventListeners].push({ event: 'frameTick', cb })
310
+ }
281
311
 
282
- // idle event
283
- if (config.hooks.idle) {
284
- const cb = () => {
285
- emit('idle', this[symbols.identifier], this)
286
- }
287
- this[symbols.rendererEventListeners].push({ event: 'idle', cb })
288
- renderer.on('idle', cb)
312
+ // idle event
313
+ if (config.hooks.idle) {
314
+ const idleCb = () => {
315
+ emit('idle', this[symbols.identifier], this, [true])
289
316
  }
317
+ const activeCb = () => {
318
+ emit('idle', this[symbols.identifier], this, [false])
319
+ }
320
+ renderer.on('idle', idleCb)
321
+ renderer.on('active', activeCb)
322
+ this[symbols.rendererEventListeners].push({ event: 'idle', cb: idleCb })
323
+ this[symbols.rendererEventListeners].push({ event: 'active', cb: activeCb })
324
+ }
290
325
 
291
- // fpsUpdate event
292
- if (config.hooks.fpsUpdate) {
293
- const cb = (r, data) => {
294
- emit('fpsUpdate', this[symbols.identifier], this, [data.fps])
295
- }
296
- this[symbols.rendererEventListeners].push({ event: 'fpsUpdate', cb })
297
- renderer.on('fpsUpdate', cb)
326
+ // fpsUpdate event
327
+ if (config.hooks.fpsUpdate) {
328
+ const cb = (_r, data) => {
329
+ emit('fpsUpdate', this[symbols.identifier], this, [data.fps])
298
330
  }
299
- })
331
+ renderer.on('fpsUpdate', cb)
332
+ this[symbols.rendererEventListeners].push({ event: 'fpsUpdate', cb })
333
+ }
300
334
 
301
335
  // inBounds event emiting a lifecycle attach event
302
336
  if (config.hooks.attach) {
@@ -63,6 +63,7 @@ export default () =>
63
63
  'content',
64
64
  'placement',
65
65
  'inspector-data',
66
+ 'holder',
66
67
  ]
67
68
  const elementConfigs = []
68
69
  const forloops = []
@@ -85,7 +86,7 @@ export default () =>
85
86
  inSlot === true ? slotComponent : component
86
87
  )
87
88
 
88
- elementConfigs[0]['color'] = component.color
89
+ elementConfigs[0]['color'] = component.bgColor
89
90
  elementConfigs[0]['w'] = component.size
90
91
  elementConfigs[0]['h'] = component.size
91
92
  elementConfigs[0]['rounded'] = component.radius
@@ -116,7 +117,7 @@ export default () =>
116
117
  },
117
118
  effects: [
118
119
  function anonymous(component, elms, context, components, rootComponent, skips, effect) {
119
- elms[0].set('color', component.color)
120
+ elms[0].set('color', component.bgColor)
120
121
  },
121
122
  function anonymous(component, elms, context, components, rootComponent, skips, effect) {
122
123
  elms[0].set('w', component.size)
@@ -130,7 +131,7 @@ export default () =>
130
131
  ],
131
132
  context: {},
132
133
  },
133
- props: { size: 40, color: 'white' },
134
+ props: { size: 40, bgColor: 'white' },
134
135
  computed: {
135
136
  radius() {
136
137
  return this.size / 2