@kudzujs/core 0.4.8 → 0.4.10

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/README.md CHANGED
@@ -160,6 +160,17 @@ Logical state persists across branch switches, while uncontrolled DOM state rese
160
160
 
161
161
  Reactive conditional DOM currently targets the HTML namespace and is rejected inside SVG or MathML.
162
162
 
163
+ Top-level immutable JSX locals can hold static or state-dependent branches:
164
+
165
+ ```tsx
166
+ const menu = open ? <MenuBar /> : <p>Menu dormant</p>
167
+ const content = open && menu
168
+
169
+ return <main>{content}</main>
170
+ ```
171
+
172
+ Kudzu compiles the local initializer to the same bounded DOM ranges as an inline condition. Reassigned, block-scoped, and keyed-list alias locals remain unsupported.
173
+
163
174
  ## Keyed Lists
164
175
 
165
176
  Map local array state directly to one keyed JSX element per item:
@@ -175,6 +186,7 @@ const [items, setItems] = useState([
175
186
  key={item.id}
176
187
  className={item.done ? "done" : "active"}
177
188
  aria-label={`${item.name} item`}
189
+ style={{ opacity: item.done ? 0.5 : 1 }}
178
190
  >
179
191
  {item.name.toUpperCase()}
180
192
  <button onClick={() => setItems(items.filter(entry => entry.id !== item.id))}>Remove</button>
@@ -182,9 +194,9 @@ const [items, setItems] = useState([
182
194
  )}</ul>
183
195
  ```
184
196
 
185
- Kudzu emits initial items as static HTML, then adds, removes, updates, and moves keyed elements directly. Existing keys move without remounting, preserving uncontrolled descendant state. Direct `item.<field>` reads use compact markers; derived item expressions compile to external ESM evaluators. Item-local handlers use direct DOM listeners and receive the latest JSON-safe item for their key, including after updates, additions, and reorders. The item remains stored once in shared list state; handler descriptors carry a placeholder that the list runtime fills when mounting or updating the keyed root.
197
+ Kudzu emits initial items as static HTML, then adds, removes, updates, styles, and moves keyed elements directly. Existing keys move without remounting, preserving uncontrolled descendant state. Direct `item.<field>` reads use compact markers; derived item expressions compile to external ESM evaluators. Item-local handlers use direct DOM listeners and receive the latest JSON-safe item for their key, including after updates, additions, and reorders. The item remains stored once in shared list state; handler descriptors carry a placeholder that the list runtime fills when mounting or updating the keyed root.
186
198
 
187
- Each item must be an ordinary plain object with a unique string or finite-number key; nested data may contain only JSON-safe arrays, ordinary plain objects, and primitive values. Null-prototype objects are rejected to preserve JSON round-trip parity. The current syntax requires a direct local-state `.map`, one identifier callback parameter, one intrinsic JSX root, and `key={item.<field>}`. Derived expressions must be pure and synchronous: item reads, literals, operators, templates, approved read-only string/array methods, deterministic `Math` methods, and `String`/`Number`/`Boolean` conversion are supported. Component state, locals, imported helpers, browser globals, Promise values, mutation, arbitrary calls, and prototype-sensitive properties are rejected. Nested conditions or lists, item spreads, component tags, dynamic styles, refs, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
199
+ Each item must be an ordinary plain object with a unique string or finite-number key; nested data may contain only JSON-safe arrays, ordinary plain objects, and primitive values. Null-prototype objects are rejected to preserve JSON round-trip parity. The current syntax requires a direct local-state `.map`, one identifier callback parameter, one intrinsic JSX root, and `key={item.<field>}`. Derived expressions must be pure and synchronous: item reads, literals, operators, templates, approved read-only string/array methods, deterministic `Math` methods, and `String`/`Number`/`Boolean` conversion are supported. Component state, locals, imported helpers, browser globals, Promise values, mutation, arbitrary calls, and prototype-sensitive properties are rejected. Nested conditions or lists, item spreads, component tags, refs, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
188
200
 
189
201
  ## Normal JavaScript
190
202
 
@@ -249,11 +261,12 @@ Supported:
249
261
  - Object DOM refs in native event handlers
250
262
  - Controlled `value` and `checked` form properties
251
263
  - Conditional child `&&` and ternary DOM patches
264
+ - Top-level immutable JSX locals
252
265
  - Direct keyed local-state lists
253
266
 
254
267
  Not implemented yet:
255
268
 
256
- - Generalized JSX-valued locals and non-direct list item expressions
269
+ - Block-scoped JSX locals and non-direct list item expressions
257
270
  - Server actions and request-time SSR
258
271
  - Imported client helpers and React package islands
259
272
  - HMR and framework DevTools
@@ -268,13 +281,13 @@ Same counter with initial value `7` and increment/decrement buttons:
268
281
 
269
282
  | Framework | Initial content | Initial JS gzip | Total output | Clean build |
270
283
  |---|---:|---:|---:|---:|
271
- | Kudzu | Yes | 393 B | 1.1 KB | **431 ms** |
272
- | Astro | Yes | **158 B** | **365 B** | 974 ms |
273
- | Svelte CSR | No | 10.5 KB | 26.9 KB | 961 ms |
274
- | Qwik CSR | No | 20.6 KB | 57.8 KB | 660 ms |
275
- | Vue CSR | No | 24.0 KB | 60.3 KB | 859 ms |
276
- | React CSR | No | 59.2 KB | 189.0 KB | 1133 ms |
277
- | Next.js | Yes | 182.1 KB | 652.2 KB | 3269 ms |
284
+ | Kudzu | Yes | 393 B | 1.1 KB | **383 ms** |
285
+ | Astro | Yes | **158 B** | **365 B** | 914 ms |
286
+ | Svelte CSR | No | 10.5 KB | 26.9 KB | 892 ms |
287
+ | Qwik CSR | No | 20.6 KB | 57.8 KB | 606 ms |
288
+ | Vue CSR | No | 24.0 KB | 60.3 KB | 815 ms |
289
+ | React CSR | No | 59.2 KB | 189.0 KB | 1044 ms |
290
+ | Next.js | Yes | 182.1 KB | 652.2 KB | 3030 ms |
278
291
 
279
292
  Astro produces the smallest hand-authored counter. Kudzu's advantage in this fixture is React-shaped state code with a sub-1 KB runtime, not the smallest possible JavaScript.
280
293
 
@@ -284,13 +297,13 @@ Same content and CSS across every fixture:
284
297
 
285
298
  | Framework | Initial content | Initial JS gzip | Total output | Clean build |
286
299
  |---|---:|---:|---:|---:|
287
- | Kudzu | Yes | **0 B** | 3.2 KB | **385 ms** |
288
- | Astro | Yes | **0 B** | **3.0 KB** | 970 ms |
289
- | Svelte CSR | No | 10.2 KB | 27.2 KB | 829 ms |
290
- | Qwik CSR | No | 20.2 KB | 59.6 KB | 634 ms |
291
- | Vue CSR | No | 24.2 KB | 62.3 KB | 767 ms |
292
- | React CSR | No | 59.8 KB | 192.3 KB | 1098 ms |
293
- | Next.js | Yes | 182.6 KB | 663.6 KB | 3217 ms |
300
+ | Kudzu | Yes | **0 B** | 3.2 KB | **409 ms** |
301
+ | Astro | Yes | **0 B** | **3.0 KB** | 1053 ms |
302
+ | Svelte CSR | No | 10.2 KB | 27.2 KB | 863 ms |
303
+ | Qwik CSR | No | 20.2 KB | 59.6 KB | 611 ms |
304
+ | Vue CSR | No | 24.2 KB | 62.3 KB | 782 ms |
305
+ | React CSR | No | 59.8 KB | 192.3 KB | 1062 ms |
306
+ | Next.js | Yes | 182.6 KB | 663.6 KB | 3118 ms |
294
307
 
295
308
  ### 1,000-item Keyed List
296
309
 
@@ -298,15 +311,15 @@ The list starts with 1,000 keyed items, then updates every label, reverses the o
298
311
 
299
312
  | Framework | Initial content | Initial JS gzip | Total output | Build | Update | Reverse | Remove | Add | Operations total |
300
313
  |---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
301
- | Astro | Yes | **324 B** | **43.6 KB** | 869 ms | **3.9 ms** | 29.4 ms | 13.9 ms | **18.3 ms** | **65.5 ms** |
302
- | Kudzu | Yes | 5.0 KB | 60.3 KB | **437 ms** | 7.0 ms | 31.7 ms | **6.7 ms** | 22.0 ms | 67.4 ms |
303
- | Vue CSR | No | 24.3 KB | 61.3 KB | 813 ms | 11.3 ms | 37.5 ms | 10.1 ms | 21.4 ms | 80.3 ms |
304
- | Next.js | Yes | 182.2 KB | 695.2 KB | 3069 ms | 7.8 ms | 40.6 ms | 9.8 ms | 22.2 ms | 80.4 ms |
305
- | React CSR | No | 59.3 KB | 189.4 KB | 1073 ms | 10.5 ms | 40.2 ms | 9.9 ms | 21.4 ms | 82.0 ms |
306
- | Qwik CSR | No | 22.2 KB | 64.1 KB | 625 ms | 13.3 ms | **25.4 ms** | 37.1 ms | 24.4 ms | 100.2 ms |
307
- | Svelte CSR | No | 12.9 KB | 33.1 KB | 905 ms | 6.5 ms | 72.3 ms | 9.2 ms | 22.3 ms | 110.3 ms |
308
-
309
- Astro is the hand-authored native DOM baseline in the interactive fixtures. React, Vue, Svelte, and Qwik used client-rendered fixtures, while Kudzu and Astro emitted initial HTML; Qwik therefore did not exercise its SSR resumability advantage. In this run Kudzu's keyed-list operations total 67.4 ms, 1.9 ms behind Astro and 14.6 ms ahead of React across all four operations.
314
+ | Astro | Yes | **324 B** | **43.6 KB** | 862 ms | **3.5 ms** | 24.8 ms | **8.4 ms** | **17.9 ms** | **54.6 ms** |
315
+ | Kudzu | Yes | 5.0 KB | 60.3 KB | **435 ms** | 6.1 ms | 29.0 ms | 12.8 ms | 19.5 ms | 67.4 ms |
316
+ | Vue CSR | No | 24.3 KB | 61.3 KB | 789 ms | 9.3 ms | 32.1 ms | 10.6 ms | 18.6 ms | 70.6 ms |
317
+ | React CSR | No | 59.3 KB | 189.4 KB | 1049 ms | 8.8 ms | 34.7 ms | 13.5 ms | 18.2 ms | 75.2 ms |
318
+ | Next.js | Yes | 182.2 KB | 695.2 KB | 3029 ms | 7.0 ms | 37.2 ms | 12.9 ms | 21.9 ms | 79.0 ms |
319
+ | Qwik CSR | No | 22.2 KB | 64.1 KB | 623 ms | 13.4 ms | **21.7 ms** | 33.6 ms | 28.9 ms | 97.6 ms |
320
+ | Svelte CSR | No | 12.9 KB | 33.1 KB | 870 ms | 5.6 ms | 61.3 ms | 15.6 ms | 18.3 ms | 100.8 ms |
321
+
322
+ Astro is the hand-authored native DOM baseline in the interactive fixtures. React, Vue, Svelte, and Qwik used client-rendered fixtures, while Kudzu and Astro emitted initial HTML; Qwik therefore did not exercise its SSR resumability advantage. In this run Kudzu's keyed-list operations total 67.4 ms, 12.8 ms behind Astro and 7.8 ms ahead of React across all four operations.
310
323
 
311
324
  Benchmark snapshot collected on July 22, 2026 with Node 24.14.0 on an Intel i5-9500. These results compare the selected one-page fixtures, not ecosystem maturity, browser interaction speed beyond the listed operations, or each framework's full rendering options. Build times vary with machine load and filesystem cache.
312
325
 
@@ -6,7 +6,7 @@
6
6
  - `runtime.js`: command-only runtime for direct state-to-text patches.
7
7
  - `shared-runtime.js`: command runtime with capability commit and DOM lifecycle hooks, emitted only when needed.
8
8
  - `binding-runtime.js`: optional generic attributes, form properties, and conditional range patches.
9
- - `list-runtime.js`: optional keyed list validation, external item-expression evaluation, dynamic item-handler scopes, moves, and cleanup.
9
+ - `list-runtime.js`: optional keyed list validation, external item-expression evaluation, dynamic styles and item-handler scopes, moves, and cleanup.
10
10
  - `serialization.js`: capture deserialization shared by binding and native handlers.
11
11
  - `native-runtime.js`: optional runtime for normal synchronous and asynchronous ESM handlers.
12
12
  - `dev-state.js`: dev-only, short-lived logical-state snapshot validation and restoration.
@@ -37,6 +37,7 @@ export async function build({ quiet = false, minify = true } = {}) {
37
37
  let behaviorCount = 0
38
38
  let bindingCount = 0
39
39
  let listCount = 0
40
+ let listStyleCount = 0
40
41
  let stateSeedCount = 0
41
42
  const plans = []
42
43
  const hasStyles = await exists(join(sourceDirectory, "style.css"))
@@ -58,6 +59,7 @@ export async function build({ quiet = false, minify = true } = {}) {
58
59
  if (result.hasBehaviors) behaviorCount++
59
60
  if (result.hasBindings) bindingCount++
60
61
  if (result.hasLists) listCount++
62
+ if (result.hasListStyles) listStyleCount++
61
63
  if (result.hasStateSeed) stateSeedCount++
62
64
  }
63
65
 
@@ -73,8 +75,8 @@ export async function build({ quiet = false, minify = true } = {}) {
73
75
  await writeJavaScript(join(assetsDirectory, "kudzu.js"), runtime, minify)
74
76
  }
75
77
  if (bindingCount || hasNativeHandlers) await writeJavaScript(join(assetsDirectory, "kudzu-serialization.js"), await readFile(new URL("./serialization.js", import.meta.url), "utf8"), minify)
78
+ if (bindingCount || listStyleCount) await writeJavaScript(join(assetsDirectory, "kudzu-style.js"), await readFile(new URL("./style.js", import.meta.url), "utf8"), minify)
76
79
  if (bindingCount) {
77
- await writeJavaScript(join(assetsDirectory, "kudzu-style.js"), await readFile(new URL("./style.js", import.meta.url), "utf8"), minify)
78
80
  const bindingRuntime = (await readFile(new URL("./binding-runtime.js", import.meta.url), "utf8"))
79
81
  .replace('"./shared-runtime.js"', '"./kudzu.js"')
80
82
  .replace('"./serialization.js"', '"./kudzu-serialization.js"')
@@ -82,8 +84,16 @@ export async function build({ quiet = false, minify = true } = {}) {
82
84
  await writeJavaScript(join(assetsDirectory, "kudzu-binding.js"), bindingRuntime, minify)
83
85
  }
84
86
  if (listCount) {
85
- const listRuntime = (await readFile(new URL("./list-runtime.js", import.meta.url), "utf8"))
87
+ let listRuntime = (await readFile(new URL("./list-runtime.js", import.meta.url), "utf8"))
86
88
  .replace('"./shared-runtime.js"', '"./kudzu.js"')
89
+ const stylePatch = ` if (target === "style") {
90
+ const style = serializeStyle(value)
91
+ if (style) node.setAttribute("style", style)
92
+ else node.removeAttribute("style")
93
+ return
94
+ }`
95
+ listRuntime = listRuntime.replace(" /* list-style */", listStyleCount ? stylePatch : "")
96
+ if (listStyleCount) listRuntime = `import { serializeStyle } from "./kudzu-style.js"\n${listRuntime}`
87
97
  await writeJavaScript(join(assetsDirectory, "kudzu-list.js"), listRuntime, minify)
88
98
  }
89
99
  if (hasNativeHandlers) {
@@ -312,6 +322,8 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
312
322
  const factory = context.factory
313
323
  const settersByFunction = new Map()
314
324
  const functions = new Map()
325
+ const jsxLocalDeclarations = new Map()
326
+ const jsxLocalsByFunction = new Map()
315
327
  const listValues = new WeakMap()
316
328
  const listEventItems = new WeakMap()
317
329
  let usesBehavior = false
@@ -336,9 +348,29 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
336
348
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
337
349
  functions.set(node.name.text, node.initializer)
338
350
  }
351
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isTopLevelConst(node)) {
352
+ const owner = nearestFunction(node)
353
+ const declarations = jsxLocalDeclarations.get(owner) ?? new Map()
354
+ declarations.set(node.name.text, node.initializer)
355
+ jsxLocalDeclarations.set(owner, declarations)
356
+ }
339
357
  ts.forEachChild(node, collect)
340
358
  }
341
359
  collect(sourceFile)
360
+ for (const [owner, declarations] of jsxLocalDeclarations) {
361
+ const names = new Set()
362
+ let changed = true
363
+ while (changed) {
364
+ changed = false
365
+ for (const [name, initializer] of declarations) {
366
+ if (!names.has(name) && isJsxLocalValue(initializer, names)) {
367
+ names.add(name)
368
+ changed = true
369
+ }
370
+ }
371
+ }
372
+ jsxLocalsByFunction.set(owner, names)
373
+ }
342
374
 
343
375
  const visitor = node => {
344
376
  if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
@@ -359,6 +391,21 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
359
391
  return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, initializer)
360
392
  }
361
393
 
394
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && jsxLocalsByFunction.get(nearestFunction(node))?.has(node.name.text) && referencesIdentifier(nearestFunction(node).body, node.name.text)) {
395
+ const parts = conditionalParts(node.initializer)
396
+ if (parts) {
397
+ const setters = settersForNode(node, settersByFunction)
398
+ const usedStates = referencedStateNames(parts.condition, setters)
399
+ const captures = captureNames(parts.condition, parts.condition, setters)
400
+ if (usedStates.size || captures.size) {
401
+ usesBehavior = true
402
+ usesConditional = true
403
+ const compiled = compileConditional(parts.kind, parts.condition, ts.visitNode(parts.truthy, visitor), ts.visitNode(parts.falsy, visitor), setters, factory, context, reactiveBindings, handlerUrl)
404
+ return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, compiled)
405
+ }
406
+ }
407
+ }
408
+
362
409
  if (ts.isJsxExpression(node) && node.expression && listValues.has(node.expression)) {
363
410
  return factory.updateJsxExpression(node, compileListValue(node.expression, listValues.get(node.expression), factory, listExpressions, handlerUrl))
364
411
  }
@@ -488,7 +535,7 @@ function validateKeyedList(parts, sourceFile, setters, listValues, listEventItem
488
535
  const field = directProperty(expression, parts.item)
489
536
  const isRootKey = ts.isJsxAttribute(node.parent) && node.parent.name.getText() === "key"
490
537
  if (field && ["__proto__", "constructor", "prototype"].includes(field)) fail(node, `Keyed list item property "${field}" is not supported`)
491
- if (field && ts.isJsxAttribute(node.parent) && ["style", "ref", "dangerouslysetinnerhtml"].includes(node.parent.name.getText().toLowerCase())) fail(node, `Keyed list item ${node.parent.name.getText()} is not supported`)
538
+ if (field && ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.getText().toLowerCase())) fail(node, `Keyed list item ${node.parent.name.getText()} is not supported`)
492
539
  if (isRootKey) return
493
540
  if (field) {
494
541
  listValues.set(node.expression, { field })
@@ -496,7 +543,7 @@ function validateKeyedList(parts, sourceFile, setters, listValues, listEventItem
496
543
  }
497
544
  if (referencesIdentifier(expression, parts.item)) {
498
545
  validateListExpression(expression, parts.item, node, fail)
499
- if (ts.isJsxAttribute(node.parent) && ["style", "ref", "dangerouslysetinnerhtml"].includes(node.parent.name.getText().toLowerCase())) fail(node, `Keyed list item ${node.parent.name.getText()} is not supported`)
546
+ if (ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.getText().toLowerCase())) fail(node, `Keyed list item ${node.parent.name.getText()} is not supported`)
500
547
  listValues.set(node.expression, { item: parts.item })
501
548
  return
502
549
  }
@@ -608,6 +655,21 @@ function unwrapExpression(node) {
608
655
  return ts.isParenthesizedExpression(node) ? unwrapExpression(node.expression) : node
609
656
  }
610
657
 
658
+ function isTopLevelConst(node) {
659
+ const list = node.parent
660
+ const statement = list?.parent
661
+ const owner = nearestFunction(node)
662
+ return ts.isVariableDeclarationList(list) && (list.flags & ts.NodeFlags.Const) !== 0 && ts.isVariableStatement(statement) && statement.parent === owner?.body
663
+ }
664
+
665
+ function isJsxLocalValue(expression, known) {
666
+ const value = unwrapExpression(expression)
667
+ if (ts.isJsxElement(value) || ts.isJsxSelfClosingElement(value) || ts.isJsxFragment(value)) return true
668
+ if (ts.isIdentifier(value)) return known.has(value.text)
669
+ const parts = conditionalParts(value)
670
+ return Boolean(parts && (isJsxLocalValue(parts.truthy, known) || isJsxLocalValue(parts.falsy, known)))
671
+ }
672
+
611
673
  function compileReactiveBinding(expression, setters, factory, context, reactiveBindings, handlerUrl) {
612
674
  return factory.createCallExpression(factory.createIdentifier("__kBinding"), undefined, compileReactiveExpression(expression, setters, factory, context, reactiveBindings, handlerUrl))
613
675
  }
@@ -43,6 +43,7 @@ export function renderPage(
43
43
  hasBehaviors: boolean
44
44
  hasBindings: boolean
45
45
  hasLists: boolean
46
+ hasListStyles: boolean
46
47
  hasStateSeed: boolean
47
48
  plan: {
48
49
  states: Array<{ id: string; name: string; initialValue: unknown }>
@@ -197,7 +197,7 @@ function serializeCapture(name, value, seen) {
197
197
  }
198
198
 
199
199
  export async function renderPage(component, metadata = {}) {
200
- renderContext = { nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listTemplate: false, listFields: undefined, states: {}, textStates: new Set(), conditionStates: new Set(), events: [], bindings: [], conditions: [], lists: [], hasBehaviors: false, hasNativeBehaviors: false, hasBindings: false, hasLists: false }
200
+ renderContext = { nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listTemplate: false, listFields: undefined, states: {}, textStates: new Set(), conditionStates: new Set(), events: [], bindings: [], conditions: [], lists: [], hasBehaviors: false, hasNativeBehaviors: false, hasBindings: false, hasLists: false, hasListStyles: false }
201
201
 
202
202
  try {
203
203
  const body = await renderNode({ type: component, props: {} })
@@ -235,6 +235,7 @@ export async function renderPage(component, metadata = {}) {
235
235
  hasBehaviors: renderContext.hasBehaviors,
236
236
  hasBindings: renderContext.hasBindings,
237
237
  hasLists: renderContext.hasLists,
238
+ hasListStyles: renderContext.hasListStyles,
238
239
  hasStateSeed: initialState.length > 0,
239
240
  plan: {
240
241
  states: Object.entries(renderContext.states).map(([id, state]) => ({ id, ...state })),
@@ -397,11 +398,13 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
397
398
  if (value?.[listFieldMarker]) {
398
399
  attributes += renderAttribute(name, value.value)
399
400
  listAttributes.push([name, value.field])
401
+ if (name === "style") renderContext.hasListStyles = true
400
402
  continue
401
403
  }
402
404
  if (value?.[listExpressionMarker]) {
403
405
  attributes += renderAttribute(name, value.value)
404
406
  listExpressionAttributes.push([name, value.module, value.handler])
407
+ if (name === "style") renderContext.hasListStyles = true
405
408
  continue
406
409
  }
407
410
  if (value?.[signalMarker] || value?.[bindingMarker]) {
@@ -290,6 +290,7 @@ function serializeItem(value) {
290
290
  }
291
291
 
292
292
  function patchBinding(node, target, value) {
293
+ /* list-style */
293
294
  if (target === "disabled") {
294
295
  node.toggleAttribute("disabled", Boolean(value))
295
296
  } else if (target === "checked") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",