@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.
@@ -18,7 +18,7 @@
18
18
  import Component from '../component.js'
19
19
  import Router, { state as routerState } from '../router/router.js'
20
20
  import symbols from '../lib/symbols.js'
21
- import Focus from '../focus.js'
21
+ import Focus from '../focus/focus.js'
22
22
 
23
23
  let hashchangeHandler = null
24
24
 
@@ -72,6 +72,7 @@ export default () =>
72
72
  'content',
73
73
  'placement',
74
74
  'inspector-data',
75
+ 'holder',
75
76
  ]
76
77
  const elementConfigs = []
77
78
  const forloops = []
@@ -65,6 +65,7 @@ export default () =>
65
65
  'content',
66
66
  'placement',
67
67
  'inspector-data',
68
+ 'holder',
68
69
  ]
69
70
  const elementConfigs = []
70
71
  const forloops = []
@@ -91,7 +92,9 @@ export default () =>
91
92
  elementConfigs[0]['h'] = parent.node.h * (100 / 100)
92
93
  elementConfigs[0]['texture'] = component.texture
93
94
  elementConfigs[0]['color'] = component.color
94
- elementConfigs[0]['effects'] = component.effects
95
+ elementConfigs[0]['rounded'] = component.rounded
96
+ elementConfigs[0]['border'] = component.border
97
+ elementConfigs[0]['shadow'] = component.shadow
95
98
  elms[0].populate(elementConfigs[0])
96
99
 
97
100
  if (inSlot === true) {
@@ -124,6 +127,15 @@ export default () =>
124
127
  function anonymous(component, elms, context, components, rootComponent, skips, effect) {
125
128
  elms[0].set('color', component.color)
126
129
  },
130
+ function anonymous(component, elms, context, components, rootComponent, skips, effect) {
131
+ elms[0].set('rounded', component.rounded)
132
+ },
133
+ function anonymous(component, elms, context, components, rootComponent, skips, effect) {
134
+ elms[0].set('border', component.border)
135
+ },
136
+ function anonymous(component, elms, context, components, rootComponent, skips, effect) {
137
+ elms[0].set('shadow', component.shadow)
138
+ },
127
139
  ],
128
140
  context: {},
129
141
  },
@@ -132,6 +144,9 @@ export default () =>
132
144
  map: undefined,
133
145
  frame: undefined,
134
146
  color: undefined,
147
+ rounded: undefined,
148
+ border: undefined,
149
+ shadow: undefined,
135
150
  },
136
151
  state() {
137
152
  return {
@@ -18,11 +18,9 @@
18
18
  import Circle from './Circle.js'
19
19
  import RouterView from './RouterView.js'
20
20
  import Sprite from './Sprite.js'
21
- import FPScounter from './FPScounter.js'
22
21
 
23
22
  export default () => ({
24
23
  Circle: Circle(),
25
24
  RouterView: RouterView(),
26
25
  Sprite: Sprite(),
27
- FPScounter: FPScounter(),
28
26
  })
@@ -24,6 +24,8 @@ import symbols from '../../lib/symbols.js'
24
24
  import Settings from '../../settings.js'
25
25
  import shaders from '../../lib/shaders/shaders.js'
26
26
 
27
+ const holderComponentMap = new WeakMap()
28
+
27
29
  /**
28
30
  * Creates a padding object from a value and direction.
29
31
  * @param {number|object|string|undefined} padding - The padding value.
@@ -523,6 +525,9 @@ const propsTransformer = {
523
525
  this.props['data'] = v
524
526
  }
525
527
  },
528
+ set holder(v) {
529
+ this.props['interactive'] = v
530
+ },
526
531
  }
527
532
 
528
533
  export const elementAttributes = Object.keys(propsTransformer)
@@ -578,6 +583,10 @@ const Element = {
578
583
  ? renderer.createTextNode({ ...textDefaults, ...this.props.props })
579
584
  : renderer.createNode(this.props.props)
580
585
 
586
+ if (this.props['holder'] === true) {
587
+ holderComponentMap.set(this.node, this.component)
588
+ }
589
+
581
590
  if (props['@loaded'] !== undefined && typeof props['@loaded'] === 'function') {
582
591
  this.node.on('loaded', (el, { type, dimensions }) => {
583
592
  props['@loaded']({ w: dimensions.w, h: dimensions.h, type }, this)
@@ -100,6 +100,7 @@ export default (App, target, settings = {}) => {
100
100
  textureMemory: textureMemorySettings(settings),
101
101
  createImageBitmapSupport: 'auto',
102
102
  targetFPS: 'maxFPS' in settings ? settings.maxFPS : 0,
103
+ platform: 'platform' in settings ? settings.platform : null,
103
104
  },
104
105
  ...(settings.advanced || {}),
105
106
  },
@@ -118,7 +119,9 @@ export default (App, target, settings = {}) => {
118
119
 
119
120
  shaderLoader()
120
121
  fontLoader()
121
- initApp()
122
+ // Defer app initialization until after the current call stack is cleared,
123
+ // allowing the renderer to finish setting up before the app starts creating components
124
+ Promise.resolve().then(initApp)
122
125
 
123
126
  return renderer
124
127
  }
@@ -15,11 +15,12 @@
15
15
  * SPDX-License-Identifier: Apache-2.0
16
16
  */
17
17
 
18
- import symbols from './lib/symbols.js'
19
- import { state } from './router/router.js'
20
- import Settings from './settings.js'
21
- import { DEFAULT_HOLD_TIMEOUT_MS } from './constants.js'
22
- import { Log } from './lib/log.js'
18
+ import symbols from '../lib/symbols.js'
19
+ import { state } from '../router/router.js'
20
+ import Settings from '../settings.js'
21
+ import { DEFAULT_HOLD_TIMEOUT_MS } from '../constants.js'
22
+ import { Log } from '../lib/log.js'
23
+ import { getAncestors } from './helpers.js'
23
24
 
24
25
  let focusedComponent = null
25
26
  let focusChain = []
@@ -99,19 +100,6 @@ export default {
99
100
  },
100
101
  }
101
102
 
102
- /**
103
- * Recursive function that retrieves the ancestors of a component
104
- * @param {Array} components
105
- * @returns array components
106
- */
107
- const getAncestors = (components) => {
108
- if (components[0][symbols.parent] !== undefined) {
109
- components.unshift(components[0][symbols.parent])
110
- return getAncestors(components)
111
- }
112
- return components
113
- }
114
-
115
103
  /**
116
104
  * Get the component in the ancestors chain that has a handler for a certain key code
117
105
  * @param {Object} component
@@ -0,0 +1,31 @@
1
+ /*
2
+ * Copyright 2025 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
+ import symbols from '../lib/symbols.js'
19
+
20
+ /**
21
+ * Recursive function that retrieves the ancestors of a component
22
+ * @param {Array} components
23
+ * @returns array components
24
+ */
25
+ export const getAncestors = (components) => {
26
+ if (components[0][symbols.parent] !== undefined) {
27
+ components.unshift(components[0][symbols.parent])
28
+ return getAncestors(components)
29
+ }
30
+ return components
31
+ }
@@ -0,0 +1,94 @@
1
+ /*
2
+ * Copyright 2025 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
+ import { Log } from '../lib/log.js'
19
+ import { getAncestors } from './helpers.js'
20
+ import symbols from '../lib/symbols.js'
21
+
22
+ let hoveredComponent = null
23
+ let hoverChain = []
24
+
25
+ export default {
26
+ get() {
27
+ return hoveredComponent
28
+ },
29
+ set(component) {
30
+ if (component === undefined || component.eol === true) return
31
+
32
+ // early return if already hovered
33
+ if (component === hoveredComponent) return
34
+
35
+ if (hoveredComponent === null) {
36
+ hoverChain = getAncestors([component])
37
+ }
38
+
39
+ // unhover currently hovered components
40
+ if (hoveredComponent !== null) {
41
+ if (hoverChain[hoverChain.length - 1] === component.$parent) {
42
+ hoverChain.push(component)
43
+ } else {
44
+ const newhoverChain = getAncestors([component])
45
+ let i = hoverChain.length
46
+ while (i--) {
47
+ // don't unhover when part of the new hover chain
48
+ if (newhoverChain.indexOf(hoverChain[i]) > -1) break
49
+ // skip if component is destroyed
50
+ if (hoverChain[i].eol !== true) {
51
+ hoverChain[i][symbols.lifecycle].state = 'unhover'
52
+ }
53
+ }
54
+ hoverChain = newhoverChain
55
+ }
56
+ }
57
+
58
+ // ensure that all components in the hover path have hover state
59
+ let i = 0
60
+ while (i < hoverChain.length - 1) {
61
+ // skip if component is destroyed
62
+ if (hoverChain[i].eol !== true) {
63
+ hoverChain[i][symbols.lifecycle].state = 'hover'
64
+ }
65
+ i++
66
+ }
67
+
68
+ setHover(component)
69
+ },
70
+ clear() {
71
+ if (hoveredComponent === null) return
72
+ for (let i = 0; i < hoverChain.length; i++) {
73
+ if (hoverChain[i].eol !== true) {
74
+ hoverChain[i][symbols.lifecycle].state = 'unhover'
75
+ }
76
+ }
77
+ hoveredComponent = null
78
+ hoverChain = []
79
+ },
80
+ }
81
+
82
+ /**
83
+ * Set the hover to the Component
84
+ * @param {Object} component - The component to hover
85
+ */
86
+ const setHover = (component) => {
87
+ Log.info(
88
+ '\nHover chain:\n',
89
+ hoverChain.map((c, index) => '\t'.repeat(index) + '↳ ' + c.$componentId).join('\n')
90
+ )
91
+
92
+ hoveredComponent = component
93
+ component[symbols.lifecycle].state = 'hover'
94
+ }
package/src/launch.js CHANGED
@@ -88,6 +88,7 @@ async function rendererVersion() {
88
88
  * @property {number} [textureProcessingTimeLimit] - Max time renderer can process textures in a frame
89
89
  * @property {Partial<import('@lightningjs/renderer').RendererMainSettings>} [advanced] - Advanced renderer settings
90
90
  * @property {boolean} [announcer] - Whether or not the announcer should be activated on initialization
91
+ * @property {boolean} [enableMouse] - Enable mouse support (hover and click-to-focus). Defaults to false.
91
92
  * @property {number} [maxFPS] - Maximum FPS
92
93
  */
93
94
 
@@ -186,6 +186,10 @@ const generateElementCode = function (
186
186
  renderCode.push(`elementConfigs[${counter}][Symbol.for('isSlot')] = true`)
187
187
  }
188
188
 
189
+ if (options.holder) {
190
+ renderCode.push(`elementConfigs[${counter}]['holder'] = true`)
191
+ }
192
+
189
193
  Object.keys(templateObject).forEach((key) => {
190
194
  if (key === 'slot') {
191
195
  renderCode.push(`
@@ -31,6 +31,8 @@ const states = [
31
31
  'ready', // fired when component instantiated, reactivity setup done and template spawned
32
32
  'focus', // fired when receiving focus (can occur multiple times)
33
33
  'unfocus', // fired when losing focus (can occur multiple times)
34
+ 'hover', // fired when receiving focus (can occur multiple times)
35
+ 'unhover', // fired when losing focus (can occur multiple times)
34
36
  'destroy', // fired when component is destroyed and removed
35
37
  'attach', // fired when entering the viewport margin and attached to the render tree
36
38
  'detach', // fired when leaving the viewport margin and detached from the render tree
@@ -90,6 +92,9 @@ export default {
90
92
  inspectorEnabled = Settings.get('inspector', false)
91
93
  }
92
94
  }
95
+ // update the built-in isHovered state variable
96
+ if (v === 'hover') this.component[symbols.state].isHovered = true
97
+ if (v === 'unhover') this.component[symbols.state].isHovered = false
93
98
  if (v === 'focus') {
94
99
  this.component[symbols.state].$hasFocus = true
95
100
  if (
package/src/lib/log.js CHANGED
@@ -46,7 +46,7 @@ const logger = (context) => {
46
46
  return (
47
47
  ((level >= 1 || (Array.isArray(level) && level.indexOf('info') > -1)) &&
48
48
  console.info.bind(
49
- window.console,
49
+ console,
50
50
  `%c ⚡️ ${context} %c ${time()}`,
51
51
  'background-color: #0284c7; color: white; padding: 3px 6px 3px 1px; border-radius: 3px',
52
52
  'color: ##94a3b8;'
@@ -61,7 +61,7 @@ const logger = (context) => {
61
61
  return (
62
62
  ((level >= 1 || (Array.isArray(level) && level.indexOf('warn') > -1)) &&
63
63
  console.warn.bind(
64
- window.console,
64
+ console,
65
65
  `%c ⚡️ ${context} %c ${time()}`,
66
66
  'background-color: #fbbf24; color: white; padding: 3px 6px 3px 1px; border-radius: 3px',
67
67
  'color: ##94a3b8;'
@@ -76,7 +76,7 @@ const logger = (context) => {
76
76
  return (
77
77
  ((level >= 1 || (Array.isArray(level) && level.indexOf('error') > -1)) &&
78
78
  console.error.bind(
79
- window.console,
79
+ console,
80
80
  `%c ⚡️ ${context} %c ${time()}`,
81
81
  'background-color: #dc2626; color: white; padding: 3px 6px 3px 1px; border-radius: 3px',
82
82
  'color: ##94a3b8;'
@@ -91,7 +91,7 @@ const logger = (context) => {
91
91
  return (
92
92
  ((level >= 2 || (Array.isArray(level) && level.indexOf('debug') > -1)) &&
93
93
  console.debug.bind(
94
- window.console,
94
+ console,
95
95
  `%c ⚡️ ${context} %c (${new Date().toLocaleTimeString([], {
96
96
  hour: '2-digit',
97
97
  minute: '2-digit',
@@ -195,7 +195,7 @@ export default (code) => {
195
195
  if (thisRefs.size > 0) {
196
196
  // Skip if already modified
197
197
  if (
198
- prop.body.includes(commentText) ||
198
+ prop.body.includes(commentText.trim()) ||
199
199
  Array.from(thisRefs).some((ref) => prop.body.startsWith(ref))
200
200
  ) {
201
201
  continue
@@ -21,7 +21,7 @@ import { reactive } from '../lib/reactivity/reactive.js'
21
21
  import symbols from '../lib/symbols.js'
22
22
  import { Log } from '../lib/log.js'
23
23
  import { stage } from '../launch.js'
24
- import Focus from '../focus.js'
24
+ import Focus from '../focus/focus.js'
25
25
  import Announcer from '../announcer/announcer.js'
26
26
  import Settings from '../settings.js'
27
27
 
@@ -216,12 +216,17 @@ const defaultOptions = {
216
216
  }
217
217
 
218
218
  const makeRouteObject = (route, overrides) => {
219
+ // FIX: exclude keepAlive from the destination route options. Unlike other
220
+ // overrides, keepAlive applies to the route being LEFT, not the destination.
221
+ // It is consumed by removeView() instead.
222
+ const { keepAlive: _keepAlive, ...destOverrides } = overrideOptions // eslint-disable-line no-unused-vars
223
+
219
224
  const cleanRoute = {
220
225
  hash: overrides.hash,
221
226
  path: route.path,
222
227
  component: route.component,
223
228
  transition: 'transition' in route ? route.transition : fadeInFadeOutTransition,
224
- options: { ...defaultOptions, ...route.options, ...overrideOptions },
229
+ options: { ...defaultOptions, ...route.options, ...destOverrides },
225
230
  announce: route.announce || false,
226
231
  hooks: route.hooks || {},
227
232
  data: { ...route.data, ...navigationData, ...overrides.queryParams },
@@ -251,7 +256,7 @@ export const navigate = async function () {
251
256
  let reuse = false
252
257
  if (preventHashChangeNavigation === false && this[symbols.parent][symbols.routes]) {
253
258
  let previousRoute = currentRoute //? Object.assign({}, currentRoute) : undefined
254
- let route = matchHash(getHash(document.location.hash), this[symbols.parent][symbols.routes])
259
+ let route = matchHash(getHash(location.hash), this[symbols.parent][symbols.routes])
255
260
 
256
261
  currentRoute = route
257
262
 
@@ -262,7 +267,11 @@ export const navigate = async function () {
262
267
  const hooks = this[symbols.parent][symbols.routerHooks]
263
268
  if (hooks.beforeEach) {
264
269
  try {
265
- beforeEachResult = await hooks.beforeEach.call(this.parent, route, previousRoute)
270
+ beforeEachResult = await hooks.beforeEach.call(
271
+ this[symbols.parent],
272
+ route,
273
+ previousRoute
274
+ )
266
275
  if (isString(beforeEachResult)) {
267
276
  currentRoute = previousRoute
268
277
  to(beforeEachResult)
@@ -303,7 +312,11 @@ export const navigate = async function () {
303
312
  let beforeHookOutput
304
313
  if (route.hooks.before) {
305
314
  try {
306
- beforeHookOutput = await route.hooks.before.call(this.parent, route, previousRoute)
315
+ beforeHookOutput = await route.hooks.before.call(
316
+ this[symbols.parent],
317
+ route,
318
+ previousRoute
319
+ )
307
320
  if (isString(beforeHookOutput)) {
308
321
  currentRoute = previousRoute
309
322
  to(beforeHookOutput)
@@ -342,8 +355,12 @@ export const navigate = async function () {
342
355
 
343
356
  // add the previous route (technically still the current route at this point)
344
357
  // into the history stack when inHistory is true and we're not navigating back
358
+ //
359
+ // FIX: use truthy check instead of `!== undefined` because matchHash()
360
+ // can return `false`, which survives `!== undefined` but has no `.options`.
345
361
  if (
346
- previousRoute !== undefined &&
362
+ previousRoute &&
363
+ previousRoute.options &&
347
364
  previousRoute.options.inHistory === true &&
348
365
  navigatingBack === false
349
366
  ) {
@@ -485,7 +502,8 @@ export const navigate = async function () {
485
502
 
486
503
  // apply out out transition on previous view if available, unless
487
504
  // we're reusing the prvious page component
488
- if (previousRoute !== undefined && reuse === false) {
505
+ // FIX: truthy guard — previousRoute can be `false` (see history-push comment above).
506
+ if (previousRoute && reuse === false) {
489
507
  // only animate when there is a previous route
490
508
  shouldAnimate = true
491
509
  const oldView = this[symbols.children].splice(1, 1).pop()
@@ -512,7 +530,7 @@ export const navigate = async function () {
512
530
  if (hooks.afterEach) {
513
531
  try {
514
532
  await hooks.afterEach.call(
515
- this.parent,
533
+ this[symbols.parent],
516
534
  route, // to
517
535
  previousRoute // from
518
536
  )
@@ -525,7 +543,7 @@ export const navigate = async function () {
525
543
  if (route.hooks.after) {
526
544
  try {
527
545
  await route.hooks.after.call(
528
- this.parent,
546
+ this[symbols.parent],
529
547
  route, // to
530
548
  previousRoute // from
531
549
  )
@@ -542,6 +560,12 @@ export const navigate = async function () {
542
560
  }
543
561
  }
544
562
 
563
+ // Clear module-level variables after removeView has consumed them.
564
+ // Placed here so it executes for all navigation flows, not only when
565
+ // previousRoute exists and reuse is false.
566
+ overrideOptions = {}
567
+ navigationData = {}
568
+
545
569
  // reset navigating indicators
546
570
  navigatingBack = false
547
571
  state.navigating = false
@@ -569,11 +593,18 @@ const removeView = async (route, view, transition, navigatingBack) => {
569
593
  }
570
594
  }
571
595
 
596
+ // Resolve effective keepAlive: runtime override from $router.to() takes precedence
597
+ // over the static route config option
598
+ const keepAlive =
599
+ overrideOptions.keepAlive !== undefined
600
+ ? overrideOptions.keepAlive
601
+ : route.options && route.options.keepAlive
602
+
572
603
  // cache the page when it's as 'keepAlive' instead of destroying
573
604
  if (
574
605
  navigatingBack === false &&
575
606
  route.options &&
576
- route.options.keepAlive === true &&
607
+ keepAlive === true &&
577
608
  route.options.inHistory === true
578
609
  ) {
579
610
  const historyItem = history[history.length - 1]
@@ -588,7 +619,7 @@ const removeView = async (route, view, transition, navigatingBack) => {
588
619
  * 2. Navigating back, and the previous route is configured with "keep alive" set to true.
589
620
  * 3. Navigating back, and the previous route is not configured with "keep alive" set to true.
590
621
  */
591
- if (route.options && (route.options.keepAlive !== true || navigatingBack === true)) {
622
+ if (route.options && (keepAlive !== true || navigatingBack === true)) {
592
623
  view.destroy()
593
624
  view = null
594
625
  }
@@ -618,11 +649,11 @@ const setOrAnimate = (node, transition, shouldAnimate = true) => {
618
649
  })
619
650
  }
620
651
 
621
- export const to = (location, data = {}, options = {}) => {
652
+ export const to = (path, data = {}, options = {}) => {
622
653
  navigationData = data
623
654
  overrideOptions = options
624
655
 
625
- window.location.hash = location
656
+ location.hash = path.replace(/^#/, '')
626
657
  }
627
658
 
628
659
  export const back = function () {