@lightningjs/blits 2.0.0 → 2.1.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,27 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.1.0
4
+
5
+ _9 apr 2026_
6
+
7
+ - Fixed left over `KeyboardEvent.code` references
8
+ - Fixed issue with allowing $-prefix to be used as text content
9
+ - Fixed issue with internal keyboard event not working on older browsers
10
+ - Added tests to router hooks
11
+ - Fixed issue with transitions not being skipped when duration is set to `0`
12
+ - Added `this.$nextTick` utility helper
13
+ - Fixed issue with transitioning colours in `border`-attribute
14
+
15
+ ## v2.0.1
16
+
17
+ _23 mar 2026_
18
+
19
+ - Fixed issue with height dimension of router view
20
+ - Fixed issue with array or rounded corners not being reactive
21
+ - Fixed issue with font not being mapped correctly when using `png` in the font config
22
+ - Removed obsolete key config
23
+ - Fixed reactivity guard in computed props to handle deep nested references more safely
24
+
3
25
  ## v2.0.0
4
26
 
5
27
  _20 mar 2026_
package/index.d.ts CHANGED
@@ -474,6 +474,13 @@ declare module '@lightningjs/blits' {
474
474
  */
475
475
  $clearDebounces: () => void
476
476
 
477
+ /**
478
+ * Defer a callback to the next tick (setTimeout 0), automatically cleaned upon component destroy
479
+ * @param callback - Function to execute on the next tick
480
+ * @param args - Arguments to pass to the callback
481
+ */
482
+ $nextTick: (callback: (...args: any[]) => void, ...args: any[]) => ReturnType<typeof setTimeout>
483
+
477
484
  /**
478
485
  * Set an interval that is automatically cleaned upon component destroy
479
486
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -118,9 +118,9 @@ const Application = (config) => {
118
118
  }
119
119
 
120
120
  keyUpHandler = (e) => {
121
- const cb = keyUpCallbacks.get(e.code)
121
+ const cb = keyUpCallbacks.get(e.keyCode)
122
122
  if (cb !== undefined && typeof cb === 'function') {
123
- keyUpCallbacks.delete(e.code)
123
+ keyUpCallbacks.delete(e.keyCode)
124
124
  cb()
125
125
  }
126
126
  clearTimeout(holdTimeout)
@@ -51,7 +51,7 @@ export default {
51
51
  if (event === null || event === undefined || event instanceof KeyboardEvent === false)
52
52
  return false
53
53
 
54
- const key = keyMap[event.key] || keyMap[event.keyCode] || event.key || event.keyCode
54
+ const key = keyMap[event.keyCode] || event.keyCode
55
55
 
56
56
  const componentWithInputEvent = getComponentWithInputEvent(this, key)
57
57
  if (componentWithInputEvent === null) return false
@@ -65,8 +65,8 @@ export default {
65
65
  cb = inputEvents.any.call(componentWithInputEvent, event)
66
66
  }
67
67
 
68
- if (cb !== undefined && event.code) {
69
- keyUpCallbacks.set(event.code, cb)
68
+ if (cb !== undefined && event.keyCode) {
69
+ keyUpCallbacks.set(event.keyCode, cb)
70
70
  }
71
71
 
72
72
  return true
@@ -152,4 +152,14 @@ export default {
152
152
  enumerable: true,
153
153
  configurable: false,
154
154
  },
155
+ $nextTick: {
156
+ value: function (fn, ...params) {
157
+ // early exit when component is marked as end of life
158
+ if (this.eol === true) return
159
+ return this.$setTimeout(fn, 0, ...params)
160
+ },
161
+ writable: false,
162
+ enumerable: true,
163
+ configurable: false,
164
+ },
155
165
  }
@@ -94,7 +94,7 @@ export default () =>
94
94
  )
95
95
 
96
96
  elementConfigs[0]['w'] = parent.node.w * (100 / 100)
97
- elementConfigs[0]['height'] = parent.node.h * (100 / 100)
97
+ elementConfigs[0]['h'] = parent.node.h * (100 / 100)
98
98
  elms[0].populate(elementConfigs[0])
99
99
 
100
100
  if (inSlot === true) {
package/src/constants.js CHANGED
@@ -18,14 +18,6 @@
18
18
  export const DEFAULT_HOLD_TIMEOUT_MS = 50
19
19
 
20
20
  export const DEFAULT_KEYMAP = {
21
- ArrowLeft: 'left',
22
- ArrowRight: 'right',
23
- ArrowUp: 'up',
24
- ArrowDown: 'down',
25
- Enter: 'enter',
26
- ' ': 'space',
27
- Backspace: 'back',
28
- Escape: 'escape',
29
21
  37: 'left',
30
22
  39: 'right',
31
23
  38: 'up',
@@ -16,7 +16,13 @@
16
16
  */
17
17
 
18
18
  import { renderer } from './launch.js'
19
- import { parseToObject, isObjectString, isArrayString, isTransition } from '../../lib/utils.js'
19
+ import {
20
+ parseToObject,
21
+ isObjectString,
22
+ isArrayString,
23
+ isTransition,
24
+ isZeroDurationTransition,
25
+ } from '../../lib/utils.js'
20
26
  import colors from '../../lib/colors/colors.js'
21
27
 
22
28
  import { Log } from '../../lib/log.js'
@@ -362,7 +368,10 @@ const propsTransformer = {
362
368
  set rounded(v) {
363
369
  this.props['rounded'] = v
364
370
  if (this.element.node !== undefined && this.elementShader === true) {
365
- if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
371
+ if (
372
+ Array.isArray(v) === false &&
373
+ (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v))))
374
+ ) {
366
375
  this.element.node.props['shader'].props = v
367
376
  } else {
368
377
  if (isArrayString(v) === true) {
@@ -374,14 +383,17 @@ const propsTransformer = {
374
383
  },
375
384
  set border(v) {
376
385
  this.props['border'] = v
386
+
377
387
  if (
378
388
  this.element.node !== undefined &&
379
389
  this.elementShader === true &&
380
390
  (typeof v === 'object' || isObjectString(v) === true)
381
391
  ) {
382
392
  v = shaders.parseProps(v)
393
+ const shader = this.element.node.props['shader']
394
+ let prefix = shader.shaderKey.startsWith('rounded') ? 'border-' : ''
383
395
  for (const key in v) {
384
- this.element.node.props['shader'].props[`border-${key}`] = v[key]
396
+ this.element.node.props['shader'].props[prefix + key] = v[key]
385
397
  }
386
398
  }
387
399
  },
@@ -393,8 +405,10 @@ const propsTransformer = {
393
405
  (typeof v === 'object' || isObjectString(v) === true)
394
406
  ) {
395
407
  v = shaders.parseProps(v)
408
+ const shader = this.element.node.props['shader']
409
+ let prefix = shader.shaderKey.startsWith('rounded') ? 'shadow-' : ''
396
410
  for (const key in v) {
397
- this.element.node.props['shader'].props[`shadow-${key}`] = v[key]
411
+ this.element.node.props['shader'].props[prefix + key] = v[key]
398
412
  }
399
413
  }
400
414
  },
@@ -660,7 +674,7 @@ const Element = {
660
674
  const propsKeys = Object.keys(this.props.props)
661
675
 
662
676
  if (propsKeys.length === 1) {
663
- if (isTransition(value) === true) {
677
+ if (isTransition(value) === true && isZeroDurationTransition(value) === false) {
664
678
  return this.animate(propsKeys[0], this.props.props[propsKeys[0]], value.transition)
665
679
  }
666
680
  // set the prop to the value on the node
@@ -668,7 +682,7 @@ const Element = {
668
682
  } else {
669
683
  for (let i = 0; i < propsKeys.length; i++) {
670
684
  // todo: fix code duplication
671
- if (isTransition(value) === true) {
685
+ if (isTransition(value) === true && isZeroDurationTransition(value) === false) {
672
686
  this.animate(propsKeys[i], this.props.props[propsKeys[i]], value.transition)
673
687
  } else {
674
688
  // set the prop to the value on the node
@@ -36,7 +36,7 @@ export default () => {
36
36
  if (type === 'sdf') {
37
37
  stage.loadFont('sdf', {
38
38
  fontFamily: font.family,
39
- atlasUrl: font.json || (font.file && font.file.replace(/\.[^.]+$/, `.${font.type}.png`)),
39
+ atlasUrl: font.png || (font.file && font.file.replace(/\.[^.]+$/, `.${font.type}.png`)),
40
40
  atlasDataUrl:
41
41
  font.json || (font.file && font.file.replace(/\.[^.]+$/, `.${font.type}.json`)),
42
42
  })
@@ -21,6 +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
25
 
25
26
  let focusedComponent = null
26
27
  let focusChain = []
@@ -95,7 +96,7 @@ export default {
95
96
  }
96
97
 
97
98
  if (cb !== undefined) {
98
- keyUpCallbacks.set(event.code, cb)
99
+ keyUpCallbacks.set(event.keyCode, cb)
99
100
  }
100
101
  },
101
102
  }
@@ -133,9 +134,7 @@ const setFocus = (component, event) => {
133
134
  component[symbols.lifecycle].state = 'focus'
134
135
 
135
136
  if (event instanceof KeyboardEvent) {
136
- const internalEvent = new KeyboardEvent('keydown', event)
137
- // @ts-ignore - this is an internal event
138
- internalEvent[symbols.internalEvent] = true
137
+ const internalEvent = new InternalKeyboardEvent('keydown', event)
139
138
  document.dispatchEvent(internalEvent)
140
139
  }
141
140
  }
@@ -116,7 +116,27 @@ export default function (templateObject = { children: [] }, devMode = false) {
116
116
  }
117
117
 
118
118
  // This is used to get only variable from expression
119
- const extractVariables = function (value) {
119
+ const extractVariables = function (value, isContentKey = false) {
120
+ if (isContentKey) {
121
+ // Strip string literals (single and double quoted) so that $variables
122
+ // embedded in plain text (e.g. "'this.$notifications.add()'") are not
123
+ // mistakenly extracted as component variable references.
124
+ const stripped = value.replace(/'[^']*'|"[^"]*"/g, '')
125
+
126
+ // A $variable is a real component reference only if:
127
+ // 1. The value starts with a bare $variable (e.g. "$notification.add(ms,)")
128
+ // 2. The stripped text contains string concatenation (e.g. "'Count: ' + $notifications.count")
129
+ // Plain text with embedded $variables (e.g. "Custom plugin with $reactive state")
130
+ // should not trigger verification.
131
+ const isBareDollarVar = /^\s*\$\$?\w+/.test(stripped)
132
+ const hasConcatExpression = /\+/.test(stripped)
133
+
134
+ if (!isBareDollarVar && !hasConcatExpression) {
135
+ return false
136
+ }
137
+ value = stripped
138
+ }
139
+
120
140
  const regEx = /\$\$?\w+(\.\w+)?/g
121
141
  const matches = value.match(regEx)
122
142
  if (matches !== null) {
@@ -127,8 +147,8 @@ const extractVariables = function (value) {
127
147
  }
128
148
  }
129
149
 
130
- const verifyVariables = function (value, renderCode, type = 'dynamic') {
131
- const variablesToBeVerified = extractVariables(value)
150
+ const verifyVariables = function (value, renderCode, isContentKey = false, type = 'dynamic') {
151
+ const variablesToBeVerified = extractVariables(value, isContentKey)
132
152
  if (variablesToBeVerified !== false) {
133
153
  for (let i = 0; i < variablesToBeVerified.length; i++) {
134
154
  let variable = variablesToBeVerified[i]
@@ -222,7 +242,7 @@ const generateElementCode = function (
222
242
  }
223
243
  // value.includes('.') === false &&
224
244
  if (isDev === true && options.component !== 'scope.' && value.includes('$')) {
225
- verifyVariables(value, renderCode, 'reactive')
245
+ verifyVariables(value, renderCode, key === ':content', 'reactive')
226
246
  }
227
247
  renderCode.push(
228
248
  `elementConfigs[${counter}]['${key.substring(1)}'] = ${interpolate(
@@ -232,7 +252,7 @@ const generateElementCode = function (
232
252
  )
233
253
  } else {
234
254
  if (isDev === true && options.component !== 'scope.' && value.includes('$')) {
235
- verifyVariables(value, renderCode)
255
+ verifyVariables(value, renderCode, key === 'content')
236
256
  }
237
257
  renderCode.push(
238
258
  `elementConfigs[${counter}]['${key}'] = ${cast(value, key, options.component)}`
@@ -0,0 +1,29 @@
1
+ import symbols from '../symbols'
2
+
3
+ /**
4
+ * Internal keyboard event class that extends the native KeyboardEvent.
5
+ * This allows us to set props on the event from the eventInitDict,
6
+ * which is not correctly implemented on some browsers.
7
+ *
8
+ * @extends {KeyboardEvent}
9
+ */
10
+ export default class InternalKeyboardEvent extends KeyboardEvent {
11
+ [symbols.internalEvent] = true
12
+ _keyCode = 0
13
+
14
+ get keyCode() {
15
+ return this._keyCode
16
+ }
17
+
18
+ /**
19
+ * @param {string} type - A string with the name of the event
20
+ * @param {KeyboardEventInit} [eventInitDict] - An object that configures the event
21
+ */
22
+ constructor(type, eventInitDict) {
23
+ super(type, eventInitDict)
24
+
25
+ if ('keyCode' in eventInitDict) {
26
+ this._keyCode = eventInitDict.keyCode
27
+ }
28
+ }
29
+ }
@@ -196,14 +196,19 @@ export default (code) => {
196
196
  // Skip if already modified
197
197
  if (
198
198
  prop.body.includes(commentText.trim()) ||
199
- Array.from(thisRefs).some((ref) => prop.body.startsWith(ref))
199
+ Array.from(thisRefs).some((ref) => {
200
+ if (prop.body.startsWith(ref)) return true
201
+ // stripped && guard: body starts with the first segment, e.g. 'this.media &&'
202
+ const firstSegEnd = ref.indexOf('.', 5)
203
+ return firstSegEnd !== -1 && prop.body.startsWith(ref.substring(0, firstSegEnd))
204
+ })
200
205
  ) {
201
206
  continue
202
207
  }
203
208
 
204
209
  // reactivity code
205
210
  const refCode = Array.from(thisRefs)
206
- .map((ref) => `${ref};`)
211
+ .map((ref) => `${toSafeRef(ref)};`)
207
212
  .join(' ')
208
213
 
209
214
  // replacement with the same whitespace
@@ -290,6 +295,18 @@ export default (code) => {
290
295
  return null
291
296
  }
292
297
 
298
+ const toSafeRef = (ref) => {
299
+ // Converts 'this.a.b.c' to 'this.a && this.a.b && this.a.b.c'
300
+ const normalized = ref.replace(/\?\./g, '.')
301
+ const segments = normalized.split('.')
302
+ if (segments.length <= 2) return normalized
303
+ const parts = []
304
+ for (let i = 2; i <= segments.length; i++) {
305
+ parts.push(segments.slice(0, i).join('.'))
306
+ }
307
+ return parts.join(' && ')
308
+ }
309
+
293
310
  const extractThisReferences = (funcBody) => {
294
311
  const thisRefs = new Set()
295
312
 
package/src/lib/utils.js CHANGED
@@ -52,6 +52,15 @@ export const isTransition = (value) => {
52
52
  return value !== null && typeof value === 'object' && 'transition' in value === true
53
53
  }
54
54
 
55
+ /**
56
+ * Checks if a transition object has a duration of zero.
57
+ * @param {any} value - The value to check.
58
+ * @returns {boolean} True if the transition duration is zero, false otherwise.
59
+ */
60
+ export const isZeroDurationTransition = (value) => {
61
+ return value.transition.duration === 0
62
+ }
63
+
55
64
  /**
56
65
  * Checks if a string is an object string (starts and ends with curly braces).
57
66
  * @param {string} str - The string to check.