diff --git a/bun.lock b/bun.lock
index 373a7d58..64887ff4 100644
--- a/bun.lock
+++ b/bun.lock
@@ -320,7 +320,7 @@
},
"packages/compiler": {
"name": "@wrnexus/compiler",
- "version": "0.8.25",
+ "version": "0.8.26",
"dependencies": {
"@wrnexus/csr": "workspace:*",
"@wrnexus/store": "workspace:*",
diff --git a/packages/compiler/package.json b/packages/compiler/package.json
index 008dd253..e80eefda 100644
--- a/packages/compiler/package.json
+++ b/packages/compiler/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/compiler",
- "version": "0.8.25",
+ "version": "0.8.26",
"type": "module",
"main": "src/index.ts",
"exports": {
diff --git a/packages/compiler/src/codegen.ts b/packages/compiler/src/codegen.ts
index 21ef53bc..992f47af 100644
--- a/packages/compiler/src/codegen.ts
+++ b/packages/compiler/src/codegen.ts
@@ -511,6 +511,19 @@ function renderLoopBody(node: ViewNode, locals: string[] = []): string {
return escLit(` ${name}`);
}
+ // Native boolean attributes inside #if/#each must use presence semantics,
+ // just like attributes on the page root. disabled="false" still disables
+ // an HTML input. Component props retain their serialized boolean value.
+ if (
+ !componentTag &&
+ !node.attrs.some((a) => a.name === "data-component") &&
+ isHtmlBooleanAttribute(name)
+ ) {
+ const expression = wholeAttributeExpression(attr.value);
+ if (expression) return "${(" + expression + ") ? " + JSON.stringify(` ${name}`) + ' : ""}';
+ if (attr.value === "false") return "";
+ }
+
return escLit(` ${name}="`) + bakeLoopAttr(attr.value, componentTag) + escLit(`"`);
})
.join("");
diff --git a/packages/compiler/test/conditional-boolean-attributes.test.ts b/packages/compiler/test/conditional-boolean-attributes.test.ts
new file mode 100644
index 00000000..61ad2c49
--- /dev/null
+++ b/packages/compiler/test/conditional-boolean-attributes.test.ts
@@ -0,0 +1,23 @@
+import { expect, test } from "bun:test";
+import { parse, generate } from "../src/index.ts";
+
+test("native boolean attributes inside conditional and loop bodies use presence semantics", async () => {
+ const source = `page Example {
+ load server account { return {} }
+ view {
+ {#if account.visible}{/if}
+ {#each account.rows as row}{/each}
+ }
+ }`;
+ const code = new Bun.Transpiler({ loader: "ts" }).transformSync(generate(parse(source)));
+ const mod = await import("data:text/javascript;base64," + Buffer.from(code).toString("base64"));
+ const html = await mod.default({
+ account: { visible: true, edit: true, rows: [{ checked: false }, { checked: true }] },
+ });
+ expect(html).not.toContain("disabled");
+ expect(html).not.toContain('checked="false"');
+ expect(html.match(/ checked/g)).toHaveLength(1);
+ expect(html).toContain("required");
+ const denied = await mod.default({ account: { visible: true, edit: false, rows: [] } });
+ expect(denied).toContain(" disabled");
+});