@kudzujs/core 0.4.6 → 0.4.7

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
@@ -108,6 +108,7 @@ The handler above increments by two and patches its bound DOM once. Inspect the
108
108
  <input value={name} onInput={event => setName(event.currentTarget.value)} />
109
109
  <input type="checkbox" checked={subscribed} onChange={event => setSubscribed(event.currentTarget.checked)} />
110
110
  <select value={theme} onChange={event => setTheme(event.currentTarget.value)} />
111
+ <div style={{ opacity: open ? 1 : 0, width: open ? 240 : 0 }} />
111
112
  ```
112
113
 
113
114
  Regular attributes use the same expressions without an allowlist:
@@ -121,7 +122,7 @@ Regular attributes use the same expressions without an allowlist:
121
122
  />
122
123
  ```
123
124
 
124
- Kudzu compiles derived expressions to external ESM and patches only the bound DOM attribute or property. `aria-*` and `data-*` boolean values serialize as `"true"` or `"false"`; ordinary false values remove the attribute. Reactive `style`, `ref`, and `dangerouslySetInnerHTML` remain unsupported.
125
+ Kudzu compiles derived expressions to external ESM and patches only the bound DOM attribute or property. `aria-*` and `data-*` boolean values serialize as `"true"` or `"false"`; ordinary false values remove the attribute. Object `style` values use React-shaped camelCase properties, add `px` to nonzero dimensional numbers, and preserve unitless properties and CSS custom properties. Reactive `ref` and `dangerouslySetInnerHTML` remain unsupported.
125
126
 
126
127
  ## Conditional DOM
127
128
 
@@ -228,6 +229,7 @@ Supported:
228
229
  - Serializable component-local captures
229
230
  - Direct text DOM patches
230
231
  - Reactive standard, `aria-*`, and `data-*` attributes
232
+ - Reactive object `style` attributes
231
233
  - Controlled `value` and `checked` form properties
232
234
  - Conditional child `&&` and ternary DOM patches
233
235
  - Direct keyed local-state lists
@@ -1,5 +1,6 @@
1
1
  import { browserState, mountDom, registerCommitter, registerMountHook, registerUnmountHook, unmountDom } from "./shared-runtime.js"
2
2
  import { deserialize } from "./serialization.js"
3
+ import { serializeStyle } from "./style.js"
3
4
 
4
5
  const imports = new Map()
5
6
  const bindingTargets = new Map()
@@ -8,7 +9,7 @@ const mountedBindings = new WeakSet()
8
9
  const mountedConditions = new WeakSet()
9
10
  const bindingRegistrations = new WeakMap()
10
11
  const conditionRegistrations = new WeakMap()
11
- const bindingTypes = ["class", "disabled", "value", "checked"]
12
+ const bindingTypes = ["class", "disabled", "value", "checked", "style"]
12
13
  const bindingSelector = [...bindingTypes.map(target => `[data-k-bind-${target}]`), "[data-k-bind-attrs]"].join(",")
13
14
 
14
15
  export function patchBinding(node, target, value) {
@@ -19,6 +20,10 @@ export function patchBinding(node, target, value) {
19
20
  } else if (target === "value") {
20
21
  const next = value == null ? "" : String(value)
21
22
  if (node.value !== next) node.value = next
23
+ } else if (target === "style") {
24
+ const style = serializeStyle(value)
25
+ if (style) node.setAttribute("style", style)
26
+ else node.removeAttribute("style")
22
27
  } else if (target === "class" && (value == null || value === false)) {
23
28
  node.removeAttribute("class")
24
29
  } else if (target === "class") {
@@ -73,9 +73,11 @@ export async function build({ quiet = false } = {}) {
73
73
  }
74
74
  if (bindingCount || hasNativeHandlers) await cp(new URL("./serialization.js", import.meta.url), join(assetsDirectory, "kudzu-serialization.js"))
75
75
  if (bindingCount) {
76
+ await cp(new URL("./style.js", import.meta.url), join(assetsDirectory, "kudzu-style.js"))
76
77
  const bindingRuntime = (await readFile(new URL("./binding-runtime.js", import.meta.url), "utf8"))
77
78
  .replace('"./shared-runtime.js"', '"./kudzu.js"')
78
79
  .replace('"./serialization.js"', '"./kudzu-serialization.js"')
80
+ .replace('"./style.js"', '"./kudzu-style.js"')
79
81
  await writeFile(join(assetsDirectory, "kudzu-binding.js"), bindingRuntime)
80
82
  }
81
83
  if (listCount) {
@@ -388,7 +390,7 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
388
390
  }
389
391
  }
390
392
 
391
- if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && !/^on/i.test(node.name.getText()) && !["style", "key", "ref", "dangerouslysetinnerhtml"].includes(node.name.getText().toLowerCase())) {
393
+ if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && !/^on/i.test(node.name.getText()) && !["key", "ref", "dangerouslysetinnerhtml"].includes(node.name.getText().toLowerCase())) {
392
394
  const expression = node.initializer.expression
393
395
  const setters = settersForNode(node, settersByFunction)
394
396
  const usedStates = referencedStateNames(expression, setters)
@@ -1,3 +1,5 @@
1
+ import { serializeStyle } from "./style.js"
2
+
1
3
  const signalMarker = Symbol("kudzu.signal")
2
4
  const behaviorMarker = Symbol("kudzu.behavior")
3
5
  const nativeBehaviorMarker = Symbol("kudzu.nativeBehavior")
@@ -352,7 +354,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
352
354
  if (rawName === "selected" && selectValue !== noSelectValue) continue
353
355
  if (/^on/i.test(rawName) && !/^on[A-Z]/.test(rawName)) throw new Error(`${rawName} must use a camelCase event handler`)
354
356
  if (rawName.toLowerCase().startsWith("data-k-")) throw new Error(`${rawName} uses Kudzu's reserved data-k-* prefix`)
355
- if (["style", "ref", "dangerouslysetinnerhtml"].includes(rawName.toLowerCase()) && (value?.[signalMarker] || value?.[bindingMarker])) {
357
+ if (["ref", "dangerouslysetinnerhtml"].includes(rawName.toLowerCase()) && (value?.[signalMarker] || value?.[bindingMarker])) {
356
358
  throw new Error(`Reactive ${rawName} is not supported`)
357
359
  }
358
360
 
@@ -377,7 +379,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
377
379
  }
378
380
 
379
381
  const name = rawName === "className" ? "class" : rawName === "htmlFor" ? "for" : rawName
380
- const propertyTarget = name === "class" || name === "disabled" || name === "value" || name === "checked"
382
+ const propertyTarget = name === "class" || name === "disabled" || name === "value" || name === "checked" || name === "style"
381
383
  if (value?.[listFieldMarker]) {
382
384
  attributes += renderAttribute(name, value.value)
383
385
  listAttributes.push([name, value.field])
@@ -409,12 +411,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
409
411
  }
410
412
 
411
413
  if (tag === "select" && name === "value") continue
412
- if (name === "style" && value && typeof value === "object") {
413
- const style = Object.entries(value).map(([property, entry]) => `${toKebabCase(property)}:${entry}`).join(";")
414
- attributes += ` style="${escapeAttribute(style)}"`
415
- } else {
416
- attributes += renderAttribute(name, value)
417
- }
414
+ attributes += renderAttribute(name, value)
418
415
  }
419
416
 
420
417
  if (attributeBindings.length) attributes += ` data-k-bind-attrs='${escapeJsonAttribute(attributeBindings)}'`
@@ -481,6 +478,10 @@ function reactiveStateIds(descriptor) {
481
478
  }
482
479
 
483
480
  function renderAttribute(name, value) {
481
+ if (name === "style") {
482
+ const style = serializeStyle(value)
483
+ return style ? ` style="${escapeAttribute(style)}"` : ""
484
+ }
484
485
  if (name === "disabled" || name === "checked") return value ? ` ${name}` : ""
485
486
  if (name === "value") return value == null ? "" : ` value="${escapeAttribute(value)}"`
486
487
  if (value == null || (value === false && !isStringBooleanAttribute(name))) return ""
@@ -525,7 +526,3 @@ function listSeed(items, fields) {
525
526
  }
526
527
  return seed
527
528
  }
528
-
529
- function toKebabCase(value) {
530
- return value.replace(/[A-Z]/g, character => `-${character.toLowerCase()}`)
531
- }
@@ -0,0 +1,23 @@
1
+ const unitless = new Set([
2
+ "animationIterationCount", "aspectRatio", "borderImageOutset", "borderImageSlice", "borderImageWidth",
3
+ "columnCount", "columns", "flex", "flexGrow", "flexShrink", "fontWeight", "gridArea", "gridColumn",
4
+ "gridColumnEnd", "gridColumnSpan", "gridColumnStart", "gridRow", "gridRowEnd", "gridRowSpan", "gridRowStart",
5
+ "lineClamp", "lineHeight", "opacity", "order", "orphans", "scale", "tabSize", "widows", "zIndex", "zoom",
6
+ "fillOpacity", "floodOpacity", "stopOpacity", "strokeDasharray", "strokeDashoffset", "strokeMiterlimit",
7
+ "strokeOpacity", "strokeWidth"
8
+ ])
9
+
10
+ export function serializeStyle(value) {
11
+ if (value == null || value === false) return ""
12
+ if (typeof value !== "object" || Array.isArray(value)) throw new Error("style must be an object")
13
+ return Object.entries(value).flatMap(([property, entry]) => {
14
+ if (entry == null || typeof entry === "boolean") return []
15
+ if (typeof entry === "number" && !Number.isFinite(entry)) throw new Error(`style.${property} must be finite`)
16
+ const suffix = typeof entry === "number" && entry !== 0 && !property.startsWith("--") && !unitless.has(property) ? "px" : ""
17
+ return `${toKebabCase(property)}:${entry}${suffix}`
18
+ }).join(";")
19
+ }
20
+
21
+ function toKebabCase(value) {
22
+ return value.replace(/[A-Z]/g, character => `-${character.toLowerCase()}`).replace(/^ms-/, "-ms-")
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",