@lightningjs/blits 1.46.0 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.47.0
4
+
5
+ _26 jan 2026_
6
+
7
+ - Fixed issue with reactivity guard vite plugin failing for commented-out code
8
+ - Fixed issue with autosizing of holder node by adding extra `eol` check
9
+ - Added support for _variants_ to the Theme plugin
10
+
3
11
  ## v1.46.0
4
12
 
5
13
  _22 jan 2026_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "1.46.0",
3
+ "version": "1.47.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -112,6 +112,8 @@ export default {
112
112
  // pass focus to the parent so we don't get lost in focus limbo
113
113
  if (this.hasFocus === true) this.parent.$focus()
114
114
 
115
+ // @todo - is this really necessary?
116
+ // This cause an issue with auto sizing of parent (and required an extra eol check there)
115
117
  for (let key in this[symbols.state]) {
116
118
  if (Array.isArray(this[symbols.state][key])) {
117
119
  this[symbols.state][key] = []
@@ -243,7 +243,11 @@ const propsTransformer = {
243
243
  },
244
244
  set w(v) {
245
245
  this.props['width'] = parsePercentage.call(this, v, 'width')
246
- if (this.___wrapper === true && this.element.component[symbols.holder] !== undefined) {
246
+ if (
247
+ this.___wrapper === true &&
248
+ this.element.component.eol !== true &&
249
+ this.element.component[symbols.holder] !== undefined
250
+ ) {
247
251
  this.element.component[symbols.holder].set('w', this.props['width'])
248
252
  }
249
253
  },
@@ -252,7 +256,11 @@ const propsTransformer = {
252
256
  },
253
257
  set h(v) {
254
258
  this.props['height'] = parsePercentage.call(this, v, 'height')
255
- if (this.___wrapper === true && this.element.component[symbols.holder] !== undefined) {
259
+ if (
260
+ this.___wrapper === true &&
261
+ this.element.component.eol !== true &&
262
+ this.element.component[symbols.holder] !== undefined
263
+ ) {
256
264
  this.element.component[symbols.holder].set('h', this.props['height'])
257
265
  }
258
266
  },
@@ -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({
@@ -18,13 +18,20 @@
18
18
  export interface ThemePlugin {
19
19
  get<T = unknown>(key: string): T | undefined
20
20
  get<T>(key: string, fallback: T): T
21
+ /**
22
+ * @deprecated
23
+ * Deprecated: use `$theme.current()` instead
24
+ */
21
25
  set(theme: string): void
26
+ current(theme: string): void
27
+ variant(theme: string): void
22
28
  }
23
29
 
24
30
  export interface ThemePluginConfig {
25
31
  themes?: Record<string, Record<string, unknown>>
26
32
  current?: string
27
33
  base?: string
34
+ variant?: string
28
35
  }
29
36
 
30
37
  declare const theme: {
@@ -20,13 +20,17 @@ import { reactive } from '../lib/reactivity/reactive.js'
20
20
 
21
21
  import Settings from '../settings.js'
22
22
 
23
- const getValueByDotNotation = (obj, base, path) => {
23
+ const getValueByDotNotation = (obj, base, variant, path) => {
24
24
  const keys = path.split('.')
25
25
 
26
26
  for (let i = 0; i < keys.length; i++) {
27
27
  const key = keys[i]
28
28
 
29
- if (obj !== null && key in obj === true) {
29
+ if (variant !== null && key in variant === true) {
30
+ obj = variant[key]
31
+ variant = variant !== null && key in variant === true ? variant[key] : null
32
+ base = base !== null && key in base === true ? base[key] : null
33
+ } else if (obj !== null && key in obj === true) {
30
34
  obj = obj[key]
31
35
  base = base !== null && key in base === true ? base[key] : null
32
36
  }
@@ -51,6 +55,7 @@ export default {
51
55
  const state = reactive(
52
56
  {
53
57
  current: 'default',
58
+ variant: null,
54
59
  },
55
60
  Settings.get('reactivityMode'),
56
61
  true
@@ -62,6 +67,7 @@ export default {
62
67
  themes = config.themes
63
68
  state.current = config.current || 'default'
64
69
  base = config.base || 'default'
70
+ state.variant = config.variant || null
65
71
  } else {
66
72
  themes = { default: config }
67
73
  }
@@ -71,8 +77,14 @@ export default {
71
77
  // reference state.current, to trigger reactive effects for
72
78
  // theme get function when current theme is modified
73
79
  state.current
80
+ state.variant
74
81
  if (themeMap[key] !== undefined) return themeMap[key]
75
- const value = getValueByDotNotation(themes[state.current], themes[base] || null, key)
82
+ const value = getValueByDotNotation(
83
+ themes[state.current],
84
+ themes[base] || null,
85
+ themes[state.variant] || null,
86
+ key
87
+ )
76
88
  if (value !== undefined) {
77
89
  // store the value for next time
78
90
  themeMap[key] = value
@@ -82,6 +94,11 @@ export default {
82
94
  },
83
95
 
84
96
  set(theme) {
97
+ Log.warn('$theme.set() is deprecated, use $theme.current() instead')
98
+ this.current(theme)
99
+ },
100
+
101
+ current(theme) {
85
102
  if (theme in themes) {
86
103
  themeMap = {}
87
104
  state.current = theme
@@ -89,6 +106,16 @@ export default {
89
106
  Log.warn(`Theme ${theme} not found`)
90
107
  }
91
108
  },
109
+
110
+ variant(theme) {
111
+ if (theme in themes) {
112
+ themeMap = {}
113
+ state.variant = theme
114
+ console.log('change variant', theme)
115
+ } else {
116
+ Log.warn(`Variant ${theme} not found`)
117
+ }
118
+ },
92
119
  }
93
120
  },
94
121
  }
@@ -482,15 +482,13 @@ export const navigate = async function () {
482
482
  }
483
483
 
484
484
  let shouldAnimate = false
485
- // Declare oldView in broader scope so it can be used in hooks below
486
- let oldView = null
487
485
 
488
486
  // apply out out transition on previous view if available, unless
489
487
  // we're reusing the prvious page component
490
488
  if (previousRoute !== undefined && reuse === false) {
491
489
  // only animate when there is a previous route
492
490
  shouldAnimate = true
493
- oldView = this[symbols.children].splice(1, 1).pop()
491
+ const oldView = this[symbols.children].splice(1, 1).pop()
494
492
  if (oldView) {
495
493
  await removeView(previousRoute, oldView, route.transition.out, navigatingBack)
496
494
  }