@lightningjs/blits 1.45.2 → 1.47.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.
@@ -37,6 +37,7 @@ export default () =>
37
37
  let slotChildCounter = 0
38
38
  let cmps = []
39
39
  elementConfigs[0] = {}
40
+ elementConfigs[0]['___wrapper'] = true
40
41
 
41
42
  elms[0] = this.element(
42
43
  { parent: parent || 'root' },
@@ -39,6 +39,7 @@ export default () =>
39
39
  let slotChildCounter = 0
40
40
  let cmps = []
41
41
  elementConfigs[0] = {}
42
+ elementConfigs[0]['___wrapper'] = true
42
43
 
43
44
  elms[0] = this.element(
44
45
  { parent: parent || 'root' },
@@ -16,96 +16,106 @@
16
16
  */
17
17
 
18
18
  import Component from '../component.js'
19
- import Router from '../router/router.js'
19
+ import Router, { state as routerState } from '../router/router.js'
20
20
  import symbols from '../lib/symbols.js'
21
21
  import Focus from '../focus.js'
22
22
 
23
23
  let hashchangeHandler = null
24
24
 
25
+ /** @typedef {{ $input?: (event: any) => boolean, $focus?: (event: any) => void }} RouterViewParent */
26
+
25
27
  export default () =>
26
- Component('RouterView', {
27
- /* eslint-disable no-unused-vars */
28
- code: {
29
- render: function anonymous(parent, component, context, components, effect, getRaw, Log) {
30
- const elms = []
31
- const elementConfigs = []
32
- const forloops = []
33
- const props = []
34
- const created = []
35
- const effects = {}
36
- const skips = []
37
- let componentType
38
- let rootComponent = component
39
- let propData
40
- let slotComponent
41
- let inSlot = false
42
- let slotChildCounter = 0
43
- let cmps = []
44
- elementConfigs[0] = {}
28
+ Component(
29
+ 'RouterView',
30
+ /** @type {any} */ ({
31
+ /* eslint-disable no-unused-vars */
32
+ code: {
33
+ render: function anonymous(parent, component, context, components, effect, getRaw, Log) {
34
+ const elms = []
35
+ const elementConfigs = []
36
+ const forloops = []
37
+ const props = []
38
+ const created = []
39
+ const effects = {}
40
+ const skips = []
41
+ let componentType
42
+ let rootComponent = component
43
+ let propData
44
+ let slotComponent
45
+ let inSlot = false
46
+ let slotChildCounter = 0
47
+ let cmps = []
48
+ elementConfigs[0] = {}
49
+ elementConfigs[0]['___wrapper'] = true
45
50
 
46
- elms[0] = this.element(
47
- { parent: parent || 'root' },
48
- inSlot === true ? slotComponent : component
49
- )
51
+ elms[0] = this.element(
52
+ { parent: parent || 'root' },
53
+ inSlot === true ? slotComponent : component
54
+ )
50
55
 
51
- elementConfigs[0]['w'] = parent.node.width * (100 / 100)
52
- elementConfigs[0]['height'] = parent.node.height * (100 / 100)
53
- elms[0].populate(elementConfigs[0])
56
+ elementConfigs[0]['w'] = parent.node.width * (100 / 100)
57
+ elementConfigs[0]['height'] = parent.node.height * (100 / 100)
58
+ elms[0].populate(elementConfigs[0])
54
59
 
55
- if (inSlot === true) {
56
- slotChildCounter -= 1
57
- }
60
+ if (inSlot === true) {
61
+ slotChildCounter -= 1
62
+ }
58
63
 
59
- return {
60
- elms,
61
- cleanup: () => {
62
- rootComponent = null
63
- propData = null
64
- slotComponent = null
65
- parent = null
66
- component = null
67
- cmps.length = 0
68
- elms.length = 0
69
- components.length = 0
70
- elementConfigs.length = 0
71
- forloops.length = 0
72
- props.length = 0
73
- skips.length = 0
74
- },
75
- }
64
+ return {
65
+ elms,
66
+ cleanup: () => {
67
+ rootComponent = null
68
+ propData = null
69
+ slotComponent = null
70
+ parent = null
71
+ component = null
72
+ cmps.length = 0
73
+ elms.length = 0
74
+ components.length = 0
75
+ elementConfigs.length = 0
76
+ forloops.length = 0
77
+ props.length = 0
78
+ skips.length = 0
79
+ },
80
+ }
81
+ },
82
+ effects: [],
83
+ context: {},
76
84
  },
77
- effects: [],
78
- context: {},
79
- },
80
- state() {
81
- return {
82
- activeView: null,
83
- }
84
- },
85
- hooks: {
86
- async ready() {
87
- if (this.parent[symbols.routerHooks] && this.parent[symbols.routerHooks].init) {
88
- await this.parent[symbols.routerHooks].init.apply(this.parent)
85
+ state() {
86
+ return {
87
+ activeView: null,
89
88
  }
90
- hashchangeHandler = () => Router.navigate.apply(this)
91
- Router.navigate.apply(this)
92
- window.addEventListener('hashchange', hashchangeHandler)
93
- },
94
- destroy() {
95
- window.removeEventListener('hashchange', hashchangeHandler, false)
96
89
  },
97
- focus() {
98
- if (this.activeView && Focus.get() === this) {
99
- this.activeView.$focus()
100
- }
90
+ hooks: {
91
+ async ready() {
92
+ if (this.parent[symbols.routerHooks] && this.parent[symbols.routerHooks].init) {
93
+ await this.parent[symbols.routerHooks].init.apply(this.parent)
94
+ }
95
+ hashchangeHandler = () => Router.navigate.apply(this)
96
+ Router.navigate.apply(this)
97
+ window.addEventListener('hashchange', hashchangeHandler)
98
+ },
99
+ destroy() {
100
+ window.removeEventListener('hashchange', hashchangeHandler, false)
101
+ },
102
+ focus() {
103
+ if (this.activeView && Focus.get() === this) {
104
+ this.activeView.$focus()
105
+ }
106
+ },
101
107
  },
102
- },
103
- input: {
104
- back(e) {
105
- const navigating = Router.back.call(this)
106
- if (navigating === false) {
107
- this.parent.$focus(e)
108
- }
108
+ input: {
109
+ back(e) {
110
+ if (routerState.backNavigation === false) {
111
+ this.parent.$input(e)
112
+ return
113
+ }
114
+ const navigating = Router.back.call(this)
115
+ if (navigating === false) {
116
+ this.parent.$focus(e)
117
+ }
118
+ },
109
119
  },
110
- },
111
- })
120
+ })
121
+ )
@@ -39,6 +39,7 @@ export default () =>
39
39
  let slotChildCounter = 0
40
40
  let cmps = []
41
41
  elementConfigs[0] = {}
42
+ elementConfigs[0]['___wrapper'] = true
42
43
 
43
44
  elms[0] = this.element(
44
45
  { parent: parent || 'root' },
@@ -243,15 +243,29 @@ const propsTransformer = {
243
243
  },
244
244
  set w(v) {
245
245
  this.props['width'] = parsePercentage.call(this, v, 'width')
246
+ if (
247
+ this.___wrapper === true &&
248
+ this.element.component.eol !== true &&
249
+ this.element.component[symbols.holder] !== undefined
250
+ ) {
251
+ this.element.component[symbols.holder].set('w', this.props['width'])
252
+ }
246
253
  },
247
254
  set width(v) {
248
- this.props['width'] = parsePercentage.call(this, v, 'width')
255
+ this.w = v
249
256
  },
250
257
  set h(v) {
251
258
  this.props['height'] = parsePercentage.call(this, v, 'height')
259
+ if (
260
+ this.___wrapper === true &&
261
+ this.element.component.eol !== true &&
262
+ this.element.component[symbols.holder] !== undefined
263
+ ) {
264
+ this.element.component[symbols.holder].set('h', this.props['height'])
265
+ }
252
266
  },
253
267
  set height(v) {
254
- this.props['height'] = parsePercentage.call(this, v, 'height')
268
+ this.h = v
255
269
  },
256
270
  set x(v) {
257
271
  this.props['x'] = parsePercentage.call(this, v, 'width')
@@ -564,6 +578,39 @@ const Element = {
564
578
  })
565
579
  }
566
580
  },
581
+ /**
582
+ * Sets framework-provided inspector metadata
583
+ * Only sets if inspector is enabled and in dev mode
584
+ * @param {Object} data - Framework inspector metadata to merge
585
+ */
586
+ setInspectorMetadata(data) {
587
+ // Early return if inspector not enabled (performance optimization)
588
+ if (inspectorEnabled !== true) {
589
+ return
590
+ }
591
+
592
+ // Early return if element is destroyed (props.props is null)
593
+ if (this.props.props === undefined || this.props.props === null) {
594
+ return
595
+ }
596
+
597
+ // Initialize data object if it doesn't exist
598
+ if (this.props['data'] === undefined) {
599
+ this.props['data'] = {}
600
+ }
601
+ if (this.props.props['data'] === undefined) {
602
+ this.props.props['data'] = {}
603
+ }
604
+
605
+ // Merge framework data (with $ prefix to prevent collisions)
606
+ Object.assign(this.props['data'], data)
607
+ Object.assign(this.props.props['data'], data)
608
+
609
+ // Sync to renderer node so inspector can see it
610
+ if (this.node !== undefined && this.node !== null) {
611
+ this.node.data = { ...this.props.props['data'] }
612
+ }
613
+ },
567
614
  /**
568
615
  * Set an individual property on the node
569
616
  *
@@ -651,6 +698,11 @@ const Element = {
651
698
  f,
652
699
  }
653
700
 
701
+ // Update inspector metadata when transition starts
702
+ if (inspectorEnabled === true) {
703
+ this.setInspectorMetadata({ $isTransitioning: true })
704
+ }
705
+
654
706
  if (transition.start !== undefined && typeof transition.start === 'function') {
655
707
  // fire transition start callback when animation really starts (depending on specified delay)
656
708
  f.once('animating', () => {
@@ -685,6 +737,12 @@ const Element = {
685
737
  }
686
738
  // remove the prop from scheduled transitions
687
739
  delete this.scheduledTransitions[prop]
740
+ // Update inspector metadata when transition ends
741
+ if (inspectorEnabled === true) {
742
+ this.setInspectorMetadata({
743
+ $isTransitioning: Object.keys(this.scheduledTransitions).length > 0,
744
+ })
745
+ }
688
746
  })
689
747
 
690
748
  // start animation
@@ -151,6 +151,10 @@ const generateElementCode = function (
151
151
 
152
152
  renderCode.push(`elementConfigs[${counter}] = {}`)
153
153
 
154
+ if (counter === 0) {
155
+ renderCode.push(`elementConfigs[${counter}]['___wrapper'] = true `)
156
+ }
157
+
154
158
  if (options.forloop) {
155
159
  renderCode.push(`if(${elm} === undefined) {`)
156
160
  }
@@ -283,9 +287,11 @@ const generateComponentCode = function (
283
287
 
284
288
  const children = templateObject.children
285
289
  delete templateObject.children
290
+ // Capture holder counter before generating element code (which may process children and increment counter)
291
+ const holderCounter = counter
286
292
  generateElementCode.call(this, templateObject, parent, { ...options, ...{ holder: true } })
287
293
 
288
- parent = options.key ? `elms[${counter}][${options.key}]` : `elms[${counter}]`
294
+ parent = options.key ? `elms[${holderCounter}][${options.key}]` : `elms[${holderCounter}]`
289
295
 
290
296
  counter++
291
297
 
@@ -354,6 +360,21 @@ const generateComponentCode = function (
354
360
  }
355
361
  `)
356
362
 
363
+ // For forloops, this code runs per instance, setting metadata for each component instance
364
+ if (isDev === true) {
365
+ const componentType = templateObject[Symbol.for('componentType')]
366
+ const holderElm = options.key
367
+ ? `elms[${holderCounter}][${options.key}]`
368
+ : `elms[${holderCounter}]`
369
+ renderCode.push(`
370
+ if (${holderElm} !== undefined && typeof ${holderElm}.setInspectorMetadata === 'function') {
371
+ ${holderElm}.setInspectorMetadata({
372
+ $componentType: '${componentType}'
373
+ })
374
+ }
375
+ `)
376
+ }
377
+
357
378
  this.cleanupCode.push(`components[${counter}] = null`)
358
379
 
359
380
  if (options.forloop) {
@@ -18,6 +18,9 @@
18
18
  import { Log } from './log.js'
19
19
  import { emit, privateEmit } from './hooks.js'
20
20
  import symbols from './symbols.js'
21
+ import Settings from '../settings.js'
22
+
23
+ let inspectorEnabled = null
21
24
 
22
25
  /**
23
26
  * List of valid lifecycle states for a component.
@@ -82,8 +85,31 @@ export default {
82
85
  // emit 'public' hook
83
86
  emit(v, this.component[symbols.identifier], this.component)
84
87
  // update the built-in hasFocus state variable
85
- if (v === 'focus') this.component[symbols.state].hasFocus = true
86
- if (v === 'unfocus') this.component[symbols.state].hasFocus = false
88
+ if (v === 'focus' || v === 'unfocus') {
89
+ if (inspectorEnabled === null) {
90
+ inspectorEnabled = Settings.get('inspector', false)
91
+ }
92
+ }
93
+ if (v === 'focus') {
94
+ this.component[symbols.state].hasFocus = true
95
+ if (
96
+ inspectorEnabled === true &&
97
+ this.component[symbols.holder] &&
98
+ typeof this.component[symbols.holder].setInspectorMetadata === 'function'
99
+ ) {
100
+ this.component[symbols.holder].setInspectorMetadata({ $hasFocus: true })
101
+ }
102
+ }
103
+ if (v === 'unfocus') {
104
+ this.component[symbols.state].hasFocus = false
105
+ if (
106
+ inspectorEnabled === true &&
107
+ this.component[symbols.holder] &&
108
+ typeof this.component[symbols.holder].setInspectorMetadata === 'function'
109
+ ) {
110
+ this.component[symbols.holder].setInspectorMetadata({ $hasFocus: false })
111
+ }
112
+ }
87
113
  }
88
114
  },
89
115
  }
@@ -35,10 +35,12 @@ const findMatchingBrace = (text, startPos) => {
35
35
  // Skip comments when not in strings
36
36
  else if (!inString && char === '/' && text[position + 1] === '/') {
37
37
  // Skip line comment
38
- position = text.indexOf('\n', position) || text.length - 1
38
+ const nextNewline = text.indexOf('\n', position + 2)
39
+ position = nextNewline === -1 ? text.length - 1 : nextNewline
39
40
  } else if (!inString && char === '/' && text[position + 1] === '*') {
40
41
  // Skip block comment
41
- position = text.indexOf('*/', position + 2) || text.length - 1
42
+ const end = text.indexOf('*/', position + 2)
43
+ position = end === -1 ? text.length - 1 : end + 1
42
44
  }
43
45
  // Count braces only outside strings and comments
44
46
  else if (!inString) {
@@ -60,6 +62,44 @@ const findComputedFunctionEnd = (computedObj, openingBracePos) => {
60
62
  return findMatchingBrace(computedObj, openingBracePos)
61
63
  }
62
64
 
65
+ // Skip whitespace + commas + comments (line and block) starting from `pos`, return next code position.
66
+ const skipNonCode = (text, pos) => {
67
+ let p = pos
68
+ while (p < text.length) {
69
+ const c = text[p]
70
+
71
+ // whitespace
72
+ if (c === ' ' || c === '\t' || c === '\n' || c === '\r') {
73
+ p++
74
+ continue
75
+ }
76
+
77
+ // separators between object members
78
+ if (c === ',') {
79
+ p++
80
+ continue
81
+ }
82
+
83
+ // line comment
84
+ if (c === '/' && text[p + 1] === '/') {
85
+ const nextNewline = text.indexOf('\n', p + 2)
86
+ p = nextNewline === -1 ? text.length : nextNewline + 1
87
+ continue
88
+ }
89
+
90
+ // block comment
91
+ if (c === '/' && text[p + 1] === '*') {
92
+ const end = text.indexOf('*/', p + 2)
93
+ p = end === -1 ? text.length : end + 2
94
+ continue
95
+ }
96
+
97
+ return p
98
+ }
99
+
100
+ return p
101
+ }
102
+
63
103
  export default (code) => {
64
104
  // Find component declarations
65
105
  const componentRegex =
@@ -83,6 +123,8 @@ export default (code) => {
83
123
 
84
124
  if (computedEnd !== -1) {
85
125
  const computedObj = configObject.substring(computedStart, computedEnd)
126
+ const computedBlockStart = computedKeywordMatch.index
127
+ const computedBlockEnd = computedEnd
86
128
 
87
129
  // Process each computed property
88
130
  const computedProps = []
@@ -93,15 +135,20 @@ export default (code) => {
93
135
  // using cursor-based parsing gives better accuracy
94
136
  while (currentPos < computedObj.length - 1) {
95
137
  // -1 to exclude the closing brace
96
- // Find the next function name
97
- const functionNameMatch = /\s*(\w+)\s*\(\)/.exec(computedObj.substring(currentPos))
138
+ // Skip whitespace/comments so we don't match commented-out computed functions
139
+ currentPos = skipNonCode(computedObj, currentPos)
140
+ if (currentPos >= computedObj.length - 1) break
141
+
142
+ // Find the next function name (computed properties are `name() { ... }`)
143
+ const functionNameMatch = /^(\w+)\s*\(\)\s*/.exec(computedObj.substring(currentPos))
98
144
  if (!functionNameMatch) break
99
145
 
100
146
  const funcName = functionNameMatch[1]
101
- const funcNamePos = currentPos + functionNameMatch.index
147
+ const funcNamePos = currentPos
102
148
 
103
149
  // Find opening brace for this function
104
- const openingBracePos = computedObj.indexOf('{', funcNamePos)
150
+ const afterSignaturePos = funcNamePos + functionNameMatch[0].length
151
+ const openingBracePos = computedObj.indexOf('{', afterSignaturePos)
105
152
  if (openingBracePos === -1) break
106
153
 
107
154
  // Find closing brace for individual computed function
@@ -133,14 +180,15 @@ export default (code) => {
133
180
  }
134
181
 
135
182
  // Store the original computed object for later replacement
136
- const originalComputedObj = configObject.substring(
137
- computedStart - 'computed: '.length,
138
- computedEnd
139
- )
183
+ const originalComputedObj = configObject.substring(computedBlockStart, computedBlockEnd)
184
+ const computedObjOffsetInOriginal = computedStart - computedBlockStart
185
+ const computedObjEndInOriginal = computedObjOffsetInOriginal + computedObj.length
140
186
 
141
187
  let modifiedComputedObj = originalComputedObj
142
188
  let hasChanges = false
143
189
 
190
+ const replacements = []
191
+
144
192
  for (const prop of computedProps) {
145
193
  const thisRefs = extractThisReferences(prop.body)
146
194
 
@@ -181,13 +229,31 @@ export default (code) => {
181
229
  prop.fullText.substring(0, openBraceIndex).match(/^(\s*)/)[0] +
182
230
  '}'
183
231
 
184
- // Replace this property in the computed object text
185
- modifiedComputedObj = modifiedComputedObj.replace(prop.fullText, modifiedPropText)
186
- hasChanges = true
232
+ replacements.push({
233
+ start: prop.startPos,
234
+ end: prop.endPos,
235
+ text: modifiedPropText,
236
+ })
187
237
  }
188
238
  }
189
239
  }
190
240
 
241
+ if (replacements.length > 0) {
242
+ let modifiedComputedInner = computedObj
243
+
244
+ replacements.sort((a, b) => b.start - a.start)
245
+ for (const r of replacements) {
246
+ modifiedComputedInner =
247
+ modifiedComputedInner.slice(0, r.start) + r.text + modifiedComputedInner.slice(r.end)
248
+ }
249
+
250
+ modifiedComputedObj =
251
+ originalComputedObj.slice(0, computedObjOffsetInOriginal) +
252
+ modifiedComputedInner +
253
+ originalComputedObj.slice(computedObjEndInOriginal)
254
+ hasChanges = true
255
+ }
256
+
191
257
  if (hasChanges) {
192
258
  // Store the modification of the entire computed object
193
259
  modifications.push({
@@ -17,6 +17,7 @@
17
17
  * @property {symbol} inputEvents
18
18
  * @property {symbol} internalEvent
19
19
  * @property {symbol} intervals
20
+ * @property {symbol} debounces
20
21
  * @property {symbol} isProxy
21
22
  * @property {symbol} launched
22
23
  * @property {symbol} level
@@ -70,6 +71,7 @@ export default {
70
71
  inputEvents: Symbol('inputEvents'),
71
72
  internalEvent: Symbol('internalEvent'),
72
73
  intervals: Symbol('intervals'),
74
+ debounces: Symbol('debounces'),
73
75
  isProxy: Symbol('isProxy'),
74
76
  launched: Symbol('launched'),
75
77
  level: Symbol('level'),
@@ -15,10 +15,7 @@
15
15
  * SPDX-License-Identifier: Apache-2.0
16
16
  */
17
17
 
18
- import type { AppStatePlugin } from '@lightningjs/blits'
19
-
20
- // Re-export AppStatePlugin for direct imports
21
- export type { AppStatePlugin }
18
+ export type AppStatePlugin<TState extends Record<string, unknown> = Record<string, unknown>> = TState
22
19
 
23
20
  declare const appState: {
24
21
  readonly name: 'appState'
@@ -21,9 +21,8 @@ export { default as appState } from './appstate.js'
21
21
  export { default as storage } from './storage/storage.js'
22
22
 
23
23
  // Re-export plugin interfaces for convenience
24
- export type { LanguagePlugin, ThemePlugin, StoragePlugin, AppStatePlugin } from '@lightningjs/blits'
25
-
26
- // Re-export option types
27
- export type { LanguagePluginOptions } from './language.js'
28
- export type { ThemePluginConfig } from './theme.js'
24
+ export type { LanguagePlugin, LanguagePluginOptions } from './language.js'
25
+ export type { ThemePlugin, ThemePluginConfig } from './theme.js'
26
+ export type { StoragePlugin } from './storage/storage.js'
27
+ export type { AppStatePlugin } from './appstate.js'
29
28
 
@@ -15,16 +15,19 @@
15
15
  * SPDX-License-Identifier: Apache-2.0
16
16
  */
17
17
 
18
- import type { LanguagePlugin } from '@lightningjs/blits'
18
+ export interface LanguagePlugin {
19
+ translate(key: string, ...replacements: any[]): string
20
+ readonly language: string
21
+ set(language: string): void
22
+ translations(translationsObject: Record<string, unknown>): void
23
+ load(file: string): Promise<void>
24
+ }
19
25
 
20
26
  export interface LanguagePluginOptions {
21
27
  file?: string
22
28
  language?: string
23
29
  }
24
30
 
25
- // Re-export LanguagePlugin for direct imports
26
- export type { LanguagePlugin }
27
-
28
31
  declare const language: {
29
32
  readonly name: 'language'
30
33
  plugin: (options?: LanguagePluginOptions) => LanguagePlugin
@@ -15,10 +15,12 @@
15
15
  * SPDX-License-Identifier: Apache-2.0
16
16
  */
17
17
 
18
- import type { StoragePlugin } from '@lightningjs/blits'
19
-
20
- // Re-export StoragePlugin for direct imports
21
- export type { StoragePlugin }
18
+ export interface StoragePlugin {
19
+ get<T = unknown>(key: string): T | null
20
+ set(key: string, value: unknown): boolean
21
+ remove(key: string): void
22
+ clear(): void
23
+ }
22
24
 
23
25
  declare const storage: {
24
26
  readonly name: 'storage'
@@ -15,17 +15,25 @@
15
15
  * SPDX-License-Identifier: Apache-2.0
16
16
  */
17
17
 
18
- import type { ThemePlugin } from '@lightningjs/blits'
18
+ export interface ThemePlugin {
19
+ get<T = unknown>(key: string): T | undefined
20
+ get<T>(key: string, fallback: T): T
21
+ /**
22
+ * @deprecated
23
+ * Deprecated: use `$theme.current()` instead
24
+ */
25
+ set(theme: string): void
26
+ current(theme: string): void
27
+ variant(theme: string): void
28
+ }
19
29
 
20
30
  export interface ThemePluginConfig {
21
31
  themes?: Record<string, Record<string, unknown>>
22
32
  current?: string
23
33
  base?: string
34
+ variant?: string
24
35
  }
25
36
 
26
- // Re-export ThemePlugin for direct imports
27
- export type { ThemePlugin }
28
-
29
37
  declare const theme: {
30
38
  readonly name: 'theme'
31
39
  plugin: (config?: ThemePluginConfig | Record<string, unknown>) => ThemePlugin