ilha 0.9.4 → 0.10.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/dist/index.js CHANGED
@@ -11,7 +11,7 @@ function exceedsMaxDepth(value, depth) {
11
11
  return false;
12
12
  }
13
13
  for (const key in value) {
14
- if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
14
+ if (!Object.hasOwn(value, key)) continue;
15
15
  if (exceedsMaxDepth(value[key], depth + 1)) return true;
16
16
  }
17
17
  return false;
@@ -60,7 +60,7 @@ function shallowEqualInput(a, b) {
60
60
  const bk = Object.keys(b);
61
61
  if (ak.length !== bk.length) return false;
62
62
  for (const k of ak) {
63
- if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
63
+ if (!Object.hasOwn(b, k)) return false;
64
64
  if (!Object.is(a[k], b[k])) return false;
65
65
  }
66
66
  return true;
@@ -103,10 +103,10 @@ function getIslandSlotTag(island) {
103
103
  if (typeof tag === "string" && tag.length > 0) return tag;
104
104
  return "div";
105
105
  }
106
- function wrapIslandSlotHtml(tag, id, propsAttr, inner) {
106
+ function wrapIslandSlotHtml({ tag, id, propsAttr, inner }) {
107
107
  return `<${tag} ${SLOT_ATTR}="${escapeHtml(id)}"${propsAttr}>${inner}</${tag}>`;
108
108
  }
109
- function isStableInlineSlotMount(initialHtml, renderedHtml, slotIds) {
109
+ function isStableInlineSlotMount({ initialHtml, renderedHtml, slotIds }) {
110
110
  if (typeof document === "undefined") return false;
111
111
  const initialRoot = parseHtmlFragment(initialHtml);
112
112
  const renderedRoot = parseHtmlFragment(renderedHtml);
@@ -121,9 +121,96 @@ function isStableInlineSlotMount(initialHtml, renderedHtml, slotIds) {
121
121
  }
122
122
  return true;
123
123
  }
124
+ /**
125
+ * Props safe to embed in `data-ilha-props`.
126
+ * Functions cannot round-trip through JSON; children are owned by the live slot
127
+ * map (client) and/or the inlined SSR subtree — never depend on the attr for them.
128
+ * RawHtml is tagged so a rare attr-only mount can revive `Symbol.for("ilha.raw")`.
129
+ */
130
+ function slotPropsForAttr(props) {
131
+ if (props === void 0) return void 0;
132
+ let out;
133
+ for (const key of Object.keys(props)) {
134
+ if (key === "children") continue;
135
+ const value = props[key];
136
+ if (typeof value === "function" || typeof value === "symbol") continue;
137
+ const encoded = encodeSlotPropValue(value);
138
+ if (encoded === void 0 && value !== void 0 && value !== null) continue;
139
+ (out ??= {})[key] = encoded;
140
+ }
141
+ return out;
142
+ }
143
+ const SLOT_RAW = Symbol.for("ilha.raw");
144
+ function isRawHtmlValue(v) {
145
+ return !!(v && typeof v === "object" && SLOT_RAW in v);
146
+ }
147
+ function encodeSlotPropValue(value, seen) {
148
+ if (value == null) return value;
149
+ if (typeof value === "function" || typeof value === "symbol") return void 0;
150
+ if (typeof value !== "object") return value;
151
+ if (isRawHtmlValue(value)) return {
152
+ __ilha: "raw",
153
+ value: value.value
154
+ };
155
+ const visited = seen ?? /* @__PURE__ */ new WeakSet();
156
+ if (visited.has(value)) throw new TypeError("encodeSlotPropValue: circular reference in slot props");
157
+ visited.add(value);
158
+ if (Array.isArray(value)) return value.map((item) => encodeSlotPropValue(item, visited)).filter((item) => item !== void 0);
159
+ if (Object.getPrototypeOf(value) !== Object.prototype) return void 0;
160
+ const obj = value;
161
+ const encoded = {};
162
+ for (const key of Object.keys(obj)) {
163
+ const next = encodeSlotPropValue(obj[key], visited);
164
+ if (next !== void 0 || obj[key] === null) encoded[key] = next;
165
+ }
166
+ return encoded;
167
+ }
168
+ function makeRawHtml(value) {
169
+ return {
170
+ [SLOT_RAW]: true,
171
+ value
172
+ };
173
+ }
174
+ function reviveSlotPropValue(value) {
175
+ if (value == null || typeof value !== "object") return value;
176
+ if (Array.isArray(value)) return value.map(reviveSlotPropValue);
177
+ const obj = value;
178
+ if (obj.__ilha === "raw" && typeof obj.value === "string") return makeRawHtml(obj.value);
179
+ const out = {};
180
+ for (const key of Object.keys(obj)) out[key] = reviveSlotPropValue(obj[key]);
181
+ return out;
182
+ }
183
+ /**
184
+ * Exact legacy shape produced when RawHtml lost its brand under JSON.stringify:
185
+ * a plain object with a single own key `value` that is a string.
186
+ */
187
+ function isLegacyRawHtmlBlob(value) {
188
+ if (!value || typeof value !== "object" || isRawHtmlValue(value)) return false;
189
+ if (Object.getPrototypeOf(value) !== Object.prototype) return false;
190
+ const keys = Object.keys(value);
191
+ return keys.length === 1 && keys[0] === "value" && typeof value.value === "string";
192
+ }
193
+ /** Revive attr-parsed props (RawHtml tags). Children stay as plain data if present. */
194
+ function reviveSlotProps(props) {
195
+ if (typeof props !== "object" || props === null || Array.isArray(props)) return {};
196
+ const out = reviveSlotPropValue(props);
197
+ if ("children" in out) out.children = reviveLegacyChildren(out.children);
198
+ return out;
199
+ }
200
+ function reviveLegacyChildren(children) {
201
+ if (!Array.isArray(children)) {
202
+ if (isLegacyRawHtmlBlob(children)) return makeRawHtml(children.value);
203
+ return reviveSlotPropValue(children);
204
+ }
205
+ return children.map((child) => {
206
+ if (isLegacyRawHtmlBlob(child)) return makeRawHtml(child.value);
207
+ return reviveSlotPropValue(child);
208
+ });
209
+ }
124
210
  /** Serialized form of slot props, matching the decoded `data-ilha-props` attr. */
125
211
  function serializeSlotProps(props) {
126
- return props === void 0 ? "" : JSON.stringify(props);
212
+ const safe = slotPropsForAttr(props);
213
+ return safe === void 0 ? "" : JSON.stringify(safe);
127
214
  }
128
215
  const MORPH_CONTROLLER_SLOT_SELECTOR = "[data-slot]";
129
216
  const MORPH_CONTROLLER_ATTRS = /* @__PURE__ */ new Set([
@@ -398,25 +485,32 @@ const PROPS_ATTR = "data-ilha-props";
398
485
  const STATE_ATTR = "data-ilha-state";
399
486
  const CSS_ATTR = "data-ilha-css";
400
487
  function buildScopedStyle(css) {
401
- return `<style ${CSS_ATTR}>@scope (:scope) to ([data-ilha]){${css.replace(/<\/style/gi, "<\\/style")}}</style>`;
488
+ const safeCss = css.replace(/<\/style/gi, "<\\/style");
489
+ return `<style ${CSS_ATTR}>@scope (:scope) to ([data-ilha]){${safeCss}}</style>`;
490
+ }
491
+ const RENDER_CTX_STACK = Symbol.for("ilha.renderCtxStack");
492
+ function renderCtxStack() {
493
+ const g = globalThis;
494
+ return g[RENDER_CTX_STACK] ??= [];
402
495
  }
403
- const renderCtxStack = [];
404
496
  function pushRenderCtx(liveHost, asyncChildren) {
405
497
  const ctx = {
406
498
  slots: /* @__PURE__ */ new Map(),
407
499
  positional: 0,
408
500
  liveHost,
409
501
  pending: asyncChildren ? /* @__PURE__ */ new Map() : void 0,
410
- binds: []
502
+ binds: [],
503
+ events: []
411
504
  };
412
- renderCtxStack.push(ctx);
505
+ renderCtxStack().push(ctx);
413
506
  return ctx;
414
507
  }
415
508
  function popRenderCtx() {
416
- renderCtxStack.pop();
509
+ renderCtxStack().pop();
417
510
  }
418
511
  function currentRenderCtx() {
419
- return renderCtxStack[renderCtxStack.length - 1];
512
+ const stack = renderCtxStack();
513
+ return stack[stack.length - 1];
420
514
  }
421
515
  function isIsland(v) {
422
516
  return typeof v === "function" && ISLAND in v;
@@ -426,7 +520,7 @@ function isIslandCall(v) {
426
520
  if (ISLAND_CALL in v) return true;
427
521
  return typeof v === "object" && "island" in v && isIsland(v.island);
428
522
  }
429
- function emitIslandSlot(island, props, key) {
523
+ function emitIslandSlot({ island, props, key }) {
430
524
  const ctx = currentRenderCtx();
431
525
  let id;
432
526
  if (key !== void 0) id = `k:${key}`;
@@ -436,23 +530,44 @@ function emitIslandSlot(island, props, key) {
436
530
  props
437
531
  });
438
532
  const slotTag = getIslandSlotTag(island);
439
- const propsAttr = props ? ` ${PROPS_ATTR}='${escapeHtml(JSON.stringify(props))}'` : "";
440
- if (ctx?.liveHost) return wrapIslandSlotHtml(slotTag, id, propsAttr, "");
533
+ const attrProps = slotPropsForAttr(props);
534
+ const propsAttr = attrProps ? ` ${PROPS_ATTR}='${escapeHtml(JSON.stringify(attrProps))}'` : "";
535
+ if (ctx?.liveHost) return wrapIslandSlotHtml({
536
+ tag: slotTag,
537
+ id,
538
+ propsAttr,
539
+ inner: ""
540
+ });
441
541
  if (ctx?.pending) {
442
542
  popRenderCtx();
443
543
  try {
444
544
  const result = island(props);
445
545
  if (result instanceof Promise) {
446
546
  ctx.pending.set(id, result.then(String));
447
- return wrapIslandSlotHtml(slotTag, id, propsAttr, asyncSlotMarker(id));
547
+ return wrapIslandSlotHtml({
548
+ tag: slotTag,
549
+ id,
550
+ propsAttr,
551
+ inner: asyncSlotMarker(id)
552
+ });
448
553
  }
449
- return wrapIslandSlotHtml(slotTag, id, propsAttr, String(result));
554
+ return wrapIslandSlotHtml({
555
+ tag: slotTag,
556
+ id,
557
+ propsAttr,
558
+ inner: String(result)
559
+ });
450
560
  } finally {
451
- renderCtxStack.push(ctx);
561
+ renderCtxStack().push(ctx);
452
562
  }
453
563
  }
454
564
  const inner = island.toString(props);
455
- return wrapIslandSlotHtml(slotTag, id, propsAttr, inner);
565
+ return wrapIslandSlotHtml({
566
+ tag: slotTag,
567
+ id,
568
+ propsAttr,
569
+ inner
570
+ });
456
571
  }
457
572
  function asyncSlotMarker(id) {
458
573
  return `<!--ilha-async:${escapeHtml(id)}-->`;
@@ -464,6 +579,15 @@ async function resolveAsyncChildren(html, pending) {
464
579
  }
465
580
  return html;
466
581
  }
582
+ function resolveSignalSetter(read, value) {
583
+ if (typeof value !== "function") return value;
584
+ const prevSub = setActiveSub(void 0);
585
+ try {
586
+ return value(read());
587
+ } finally {
588
+ setActiveSub(prevSub);
589
+ }
590
+ }
467
591
  function markSignalAccessor(fn) {
468
592
  fn[SIGNAL_ACCESSOR] = true;
469
593
  const accessor = fn;
@@ -481,14 +605,19 @@ function getAtPath(obj, path) {
481
605
  }
482
606
  return cur;
483
607
  }
484
- function setAtPath(obj, path, value) {
608
+ function setAtPath({ object, path, value }) {
609
+ const obj = object;
485
610
  if (path.length === 0) return value;
486
611
  const [head, ...rest] = path;
487
612
  if (Array.isArray(obj)) {
488
613
  const idx = head;
489
614
  if (idx < 0 || idx >= obj.length) return obj;
490
615
  const next = obj[idx];
491
- const updated = rest.length === 0 ? value : setAtPath(next, rest, value);
616
+ const updated = rest.length === 0 ? value : setAtPath({
617
+ object: next,
618
+ path: rest,
619
+ value
620
+ });
492
621
  if (Object.is(next, updated)) return obj;
493
622
  const copy = obj.slice();
494
623
  copy[idx] = updated;
@@ -498,7 +627,11 @@ function setAtPath(obj, path, value) {
498
627
  const record = obj;
499
628
  const key = String(head);
500
629
  const next = record[key];
501
- const updated = rest.length === 0 ? value : setAtPath(next, rest, value);
630
+ const updated = rest.length === 0 ? value : setAtPath({
631
+ object: next,
632
+ path: rest,
633
+ value
634
+ });
502
635
  if (rest.length === 0) {
503
636
  if (Object.is(next, value)) return obj;
504
637
  } else if (Object.is(next, updated)) return obj;
@@ -507,7 +640,11 @@ function setAtPath(obj, path, value) {
507
640
  [key]: updated
508
641
  };
509
642
  }
510
- return rest.length === 0 ? value : setAtPath(void 0, rest, value);
643
+ return rest.length === 0 ? value : setAtPath({
644
+ object: void 0,
645
+ path: rest,
646
+ value
647
+ });
511
648
  }
512
649
  function toPathSegment(prop) {
513
650
  if (typeof prop === "symbol") return null;
@@ -536,8 +673,15 @@ function createSelectAccessor(root, selector) {
536
673
  if (!Object.is(selected, resolved)) throw new Error("select(): selector must only traverse nested properties or array indexes — derived or transformed values are not supported.");
537
674
  return markSignalAccessor((...args) => {
538
675
  if (args.length === 0) return path.length === 0 ? selector(root()) : getAtPath(root(), path);
539
- const next = path.length === 0 ? args[0] : setAtPath(root(), path, args[0]);
540
- if (!Object.is(root(), next)) root(next);
676
+ const previousRoot = root();
677
+ const previousSelected = path.length === 0 ? selector(previousRoot) : getAtPath(previousRoot, path);
678
+ const value = resolveSignalSetter(() => previousSelected, args[0]);
679
+ const next = path.length === 0 ? value : setAtPath({
680
+ object: previousRoot,
681
+ path,
682
+ value
683
+ });
684
+ if (!Object.is(previousRoot, next)) root((() => next));
541
685
  });
542
686
  }
543
687
  function ilhaRaw(value) {
@@ -561,9 +705,17 @@ function interpolateValue(v) {
561
705
  if (isRawHtml(v)) return v.value;
562
706
  if (isIslandCall(v)) {
563
707
  const call = typeof v === "function" ? v() : v;
564
- return emitIslandSlot(call.island, call.props, call.key);
708
+ return emitIslandSlot({
709
+ island: call.island,
710
+ props: call.props,
711
+ key: call.key
712
+ });
565
713
  }
566
- if (isIsland(v)) return emitIslandSlot(v, void 0, void 0);
714
+ if (isIsland(v)) return emitIslandSlot({
715
+ island: v,
716
+ props: void 0,
717
+ key: void 0
718
+ });
567
719
  if (isSignalAccessor(v)) return escapeHtml(v());
568
720
  if (typeof v === "function") return escapeHtml(v());
569
721
  return escapeHtml(v);
@@ -579,6 +731,30 @@ const BIND_VALID_KINDS = /* @__PURE__ */ new Set([
579
731
  "this"
580
732
  ]);
581
733
  const BIND_PREFIX_RE = /\bbind:([a-zA-Z]+)\s*=\s*("|')?$/;
734
+ const EVENT_PREFIX_RE = /(?:^|\s)on([a-z][a-z0-9-]*)(?::([a-z][a-z0-9-]*))?\s*=\s*("|')?$/;
735
+ const NATIVE_EVENT_MODIFIERS = /* @__PURE__ */ new Set([
736
+ "abortable",
737
+ "once",
738
+ "capture",
739
+ "passive"
740
+ ]);
741
+ function isEventAttributePosition(source, index) {
742
+ let open = -1;
743
+ let quote = null;
744
+ for (let i = 0; i < index; i++) {
745
+ const char = source[i];
746
+ if (open === -1) {
747
+ if (char === "<") open = i;
748
+ continue;
749
+ }
750
+ if (quote) {
751
+ if (char === quote) quote = null;
752
+ } else if (char === "\"" || char === "'") quote = char;
753
+ else if (char === ">") open = -1;
754
+ }
755
+ if (open === -1 || quote !== null) return false;
756
+ return !/^\s*[!/?]/.test(source.slice(open + 1, index));
757
+ }
582
758
  function findTagCloseIndex(chunk, initialQuote) {
583
759
  let quote = initialQuote;
584
760
  for (let i = 0; i < chunk.length; i++) {
@@ -613,7 +789,7 @@ function extractElementStaticValue(prefix) {
613
789
  if (!m) return null;
614
790
  return m[2] ?? m[3] ?? m[4] ?? null;
615
791
  }
616
- function emitBindSSR(kind, index, accessor, prefixForStaticPeek) {
792
+ function emitBindSSR({ kind, index, accessor, prefixForStaticPeek }) {
617
793
  const spec = `${kind}:${index}`;
618
794
  let v;
619
795
  try {
@@ -640,6 +816,7 @@ function ilhaHtml(strings, ...values) {
640
816
  let result = "";
641
817
  let stripLeadingQuote = null;
642
818
  let pendingBindSpecs = "";
819
+ let pendingEventSpecs = "";
643
820
  let pendingQuote = null;
644
821
  const ctx = currentRenderCtx();
645
822
  for (let i = 0; i < strings.length; i++) {
@@ -648,11 +825,15 @@ function ilhaHtml(strings, ...values) {
648
825
  if (chunk.startsWith(stripLeadingQuote)) chunk = chunk.slice(1);
649
826
  stripLeadingQuote = null;
650
827
  }
651
- if (pendingBindSpecs !== "") {
828
+ if (pendingBindSpecs !== "" || pendingEventSpecs !== "") {
652
829
  const { index: gtIdx, quote } = findTagCloseIndex(chunk, pendingQuote);
653
830
  if (gtIdx !== -1) {
654
- chunk = chunk.slice(0, gtIdx).replace(/\s*\/\s*$/, "") + ` data-ilha-bind="${pendingBindSpecs}">` + chunk.slice(gtIdx + 1);
831
+ const head = chunk.slice(0, gtIdx).replace(/\s*\/\s*$/, "");
832
+ const bindAttr = pendingBindSpecs ? ` data-ilha-bind="${pendingBindSpecs}"` : "";
833
+ const eventAttr = pendingEventSpecs ? ` data-ilha-on="${pendingEventSpecs}"` : "";
834
+ chunk = head + bindAttr + eventAttr + ">" + chunk.slice(gtIdx + 1);
655
835
  pendingBindSpecs = "";
836
+ pendingEventSpecs = "";
656
837
  pendingQuote = null;
657
838
  } else pendingQuote = quote;
658
839
  }
@@ -661,6 +842,29 @@ function ilhaHtml(strings, ...values) {
661
842
  continue;
662
843
  }
663
844
  const value = values[i];
845
+ const eventMatch = chunk.match(EVENT_PREFIX_RE);
846
+ if (eventMatch && isEventAttributePosition(result + chunk, result.length + chunk.length - eventMatch[0].length)) {
847
+ const eventName = eventMatch[1];
848
+ const rawModifier = eventMatch[2];
849
+ const modifier = NATIVE_EVENT_MODIFIERS.has(rawModifier) ? rawModifier : void 0;
850
+ const openQuote = eventMatch[3] ?? null;
851
+ const matchStart = chunk.length - eventMatch[0].length;
852
+ const cleanPrefix = chunk.slice(0, matchStart).replace(/\s+$/, "");
853
+ result += cleanPrefix;
854
+ if (ctx && typeof value === "function") {
855
+ const index = ctx.events.length;
856
+ if (rawModifier && !modifier) {} else {
857
+ ctx.events.push({
858
+ type: eventName,
859
+ handler: value,
860
+ modifier
861
+ });
862
+ pendingEventSpecs += (pendingEventSpecs ? "," : "") + `${eventName}:${index}`;
863
+ }
864
+ }
865
+ if (openQuote) stripLeadingQuote = openQuote;
866
+ continue;
867
+ }
664
868
  const m = chunk.match(BIND_PREFIX_RE);
665
869
  if (m && isSignalAccessor(value)) {
666
870
  const name = m[1];
@@ -683,7 +887,12 @@ function ilhaHtml(strings, ...values) {
683
887
  kind: name,
684
888
  accessor: value
685
889
  });
686
- const [valueAttrs, spec] = emitBindSSR(name, index, value, cleanPrefix);
890
+ const [valueAttrs, spec] = emitBindSSR({
891
+ kind: name,
892
+ index,
893
+ accessor: value,
894
+ prefixForStaticPeek: cleanPrefix
895
+ });
687
896
  result += cleanPrefix + valueAttrs;
688
897
  pendingBindSpecs += (pendingBindSpecs ? "," : "") + spec;
689
898
  if (openQuote) stripLeadingQuote = openQuote;
@@ -692,6 +901,7 @@ function ilhaHtml(strings, ...values) {
692
901
  result += chunk + interpolateValue(value);
693
902
  }
694
903
  if (pendingBindSpecs !== "") result += ` data-ilha-bind="${pendingBindSpecs}"`;
904
+ if (pendingEventSpecs !== "") result += ` data-ilha-on="${pendingEventSpecs}"`;
695
905
  return {
696
906
  [RAW]: true,
697
907
  value: dedentString(result)
@@ -710,7 +920,7 @@ function ilhaContextFn(key, initial) {
710
920
  const s = signal(initial);
711
921
  const accessor = (...args) => {
712
922
  if (args.length === 0) return s();
713
- s(args[0]);
923
+ s(resolveSignalSetter(() => s(), args[0]));
714
924
  };
715
925
  contextRegistry.set(key, accessor);
716
926
  return accessor;
@@ -740,7 +950,7 @@ function ilhaSignal(initial) {
740
950
  const s = signal(initial);
741
951
  return markSignalAccessor((...args) => {
742
952
  if (args.length === 0) return s();
743
- s(args[0]);
953
+ s(resolveSignalSetter(() => s(), args[0]));
744
954
  });
745
955
  }
746
956
  /**
@@ -867,7 +1077,7 @@ function buildDerivedAccessors(envelopes) {
867
1077
  return accessors.get(key) ?? defaultDerivedAccessor();
868
1078
  } });
869
1079
  }
870
- function createDerivedProxy(entries, state, input, derivedSnapshot) {
1080
+ function createDerivedProxy({ entries, state, input, derivedSnapshot }) {
871
1081
  const envelopes = /* @__PURE__ */ new Map();
872
1082
  for (const entry of entries) {
873
1083
  let initialEnvelope;
@@ -974,9 +1184,10 @@ function createDerivedProxy(entries, state, input, derivedSnapshot) {
974
1184
  return;
975
1185
  }
976
1186
  const prevSub = setActiveSub(void 0);
1187
+ const prevVal = env();
977
1188
  env({
978
1189
  loading: true,
979
- value: env().value,
1190
+ value: prevVal.value,
980
1191
  error: void 0
981
1192
  });
982
1193
  setActiveSub(prevSub);
@@ -1009,6 +1220,7 @@ function createDerivedProxy(entries, state, input, derivedSnapshot) {
1009
1220
  };
1010
1221
  }
1011
1222
  const BIND_SENTINEL_ATTR = "data-ilha-bind";
1223
+ const EVENT_SENTINEL_ATTR = "data-ilha-on";
1012
1224
  function parseBindSentinel(value) {
1013
1225
  const out = [];
1014
1226
  for (const part of value.split(",")) {
@@ -1088,12 +1300,86 @@ function resolveBindOps(el, kind) {
1088
1300
  }
1089
1301
  }
1090
1302
  }
1303
+ function templateElementBelongsToHost(host, candidate) {
1304
+ if (candidate === host) return true;
1305
+ let current = candidate;
1306
+ while (current && current !== host) {
1307
+ if (current.hasAttribute(SLOT_ATTR) || current.hasAttribute("data-ilha")) return false;
1308
+ current = current.parentElement;
1309
+ }
1310
+ return current === host;
1311
+ }
1312
+ function applyJsxEvents({ host, records, reportError, unmountSignal, onceFired }) {
1313
+ if (records.length === 0) return () => {};
1314
+ const cleanups = [];
1315
+ const elements = [];
1316
+ if (host.hasAttribute(EVENT_SENTINEL_ATTR)) elements.push(host);
1317
+ elements.push(...Array.from(host.querySelectorAll(`[${EVENT_SENTINEL_ATTR}]`)).filter((element) => templateElementBelongsToHost(host, element)));
1318
+ for (const el of elements) {
1319
+ const sentinel = el.getAttribute(EVENT_SENTINEL_ATTR) ?? "";
1320
+ for (const part of sentinel.split(",")) {
1321
+ const separator = part.lastIndexOf(":");
1322
+ if (separator < 1) continue;
1323
+ const type = part.slice(0, separator);
1324
+ const index = Number(part.slice(separator + 1));
1325
+ if (!Number.isInteger(index) || index < 0) continue;
1326
+ const record = records[index];
1327
+ if (!record || record.type !== type) continue;
1328
+ if (record.modifier === "once" && onceFired.get(el)?.has(type)) continue;
1329
+ const listenerController = new AbortController();
1330
+ let invocationController;
1331
+ const options = {
1332
+ once: record.modifier === "once",
1333
+ capture: record.modifier === "capture",
1334
+ passive: record.modifier === "passive"
1335
+ };
1336
+ const reportHandlerError = (error) => {
1337
+ if (error?.name !== "AbortError") reportError(error);
1338
+ };
1339
+ const listener = (event) => {
1340
+ if (record.modifier === "once") {
1341
+ let fired = onceFired.get(el);
1342
+ if (!fired) onceFired.set(el, fired = /* @__PURE__ */ new Set());
1343
+ fired.add(type);
1344
+ }
1345
+ if (record.modifier === "abortable") {
1346
+ invocationController?.abort();
1347
+ invocationController = new AbortController();
1348
+ }
1349
+ const signal = invocationController ? anySignal([listenerController.signal, invocationController.signal]) : listenerController.signal;
1350
+ let result;
1351
+ startBatch();
1352
+ try {
1353
+ result = record.handler(event, { signal });
1354
+ } catch (error) {
1355
+ reportHandlerError(error);
1356
+ return;
1357
+ } finally {
1358
+ endBatch();
1359
+ }
1360
+ if (result != null && typeof result.then === "function") Promise.resolve(result).catch(reportHandlerError);
1361
+ };
1362
+ const stop = () => {
1363
+ listenerController.abort();
1364
+ invocationController?.abort();
1365
+ el.removeEventListener(type, listener, options);
1366
+ };
1367
+ el.addEventListener(type, listener, options);
1368
+ unmountSignal.addEventListener("abort", stop, { once: true });
1369
+ cleanups.push(() => {
1370
+ unmountSignal.removeEventListener("abort", stop);
1371
+ stop();
1372
+ });
1373
+ }
1374
+ }
1375
+ return () => cleanups.forEach((cleanup) => cleanup());
1376
+ }
1091
1377
  function applyTemplateBindings(host, binds) {
1092
1378
  if (binds.length === 0) return () => {};
1093
1379
  const cleanups = [];
1094
1380
  const elements = [];
1095
1381
  if (host.hasAttribute(BIND_SENTINEL_ATTR)) elements.push(host);
1096
- for (const el of host.querySelectorAll(`[${BIND_SENTINEL_ATTR}]`)) elements.push(el);
1382
+ for (const el of host.querySelectorAll(`[${BIND_SENTINEL_ATTR}]`)) if (templateElementBelongsToHost(host, el)) elements.push(el);
1097
1383
  for (const el of elements) {
1098
1384
  const specs = parseBindSentinel(el.getAttribute(BIND_SENTINEL_ATTR));
1099
1385
  for (const spec of specs) {
@@ -1235,6 +1521,7 @@ var IlhaBuilder = class IlhaBuilder {
1235
1521
  defaultInput,
1236
1522
  states: [],
1237
1523
  deriveds: [],
1524
+ actions: [],
1238
1525
  ons: [],
1239
1526
  effects: [],
1240
1527
  onMounts: [],
@@ -1270,6 +1557,15 @@ var IlhaBuilder = class IlhaBuilder {
1270
1557
  }]
1271
1558
  });
1272
1559
  }
1560
+ action(key, fn) {
1561
+ return new IlhaBuilder({
1562
+ ...this._cfg,
1563
+ actions: [...this._cfg.actions, {
1564
+ key,
1565
+ fn
1566
+ }]
1567
+ });
1568
+ }
1273
1569
  on(selectorOrCombined, handler) {
1274
1570
  const parsed = parseOnArgs(selectorOrCombined);
1275
1571
  return new IlhaBuilder({
@@ -1324,7 +1620,7 @@ var IlhaBuilder = class IlhaBuilder {
1324
1620
  });
1325
1621
  }
1326
1622
  render(fn) {
1327
- const { schema, defaultInput, states, deriveds, ons, effects, onMounts, onErrors, transition, css: cssSource, as: slotAs } = this._cfg;
1623
+ const { schema, defaultInput, states, deriveds, actions, ons, effects, onMounts, onErrors, transition, css: cssSource, as: slotAs } = this._cfg;
1328
1624
  const stylePrefix = cssSource != null ? buildScopedStyle(cssSource) : "";
1329
1625
  const configuredSlotTag = slotAs ?? "div";
1330
1626
  function resolveInput(props) {
@@ -1335,26 +1631,29 @@ var IlhaBuilder = class IlhaBuilder {
1335
1631
  if (!schema) return merged;
1336
1632
  return validateSchema(schema, merged);
1337
1633
  }
1338
- function renderWithCtx(fn, liveHost, asyncChildren) {
1634
+ function renderWithCtx({ render, liveHost, asyncChildren }) {
1339
1635
  const ctx = pushRenderCtx(liveHost, asyncChildren);
1340
1636
  try {
1341
- const html = unwrapHtml(fn());
1637
+ const html = unwrapHtml(render());
1342
1638
  const slots = ctx.slots;
1343
1639
  const binds = ctx.binds;
1640
+ const events = ctx.events;
1344
1641
  if (ctx.pending && ctx.pending.size > 0) {
1345
1642
  const pending = ctx.pending;
1346
1643
  return (async () => {
1347
1644
  return {
1348
1645
  html: await resolveAsyncChildren(html, pending),
1349
1646
  slots,
1350
- binds
1647
+ binds,
1648
+ events
1351
1649
  };
1352
1650
  })();
1353
1651
  }
1354
1652
  return {
1355
1653
  html,
1356
1654
  slots,
1357
- binds
1655
+ binds,
1656
+ events
1358
1657
  };
1359
1658
  } finally {
1360
1659
  popRenderCtx();
@@ -1377,12 +1676,34 @@ var IlhaBuilder = class IlhaBuilder {
1377
1676
  const s = signal(snapshot && entry.key in snapshot ? snapshot[entry.key] : typeof entry.init === "function" ? entry.init(input) : entry.init);
1378
1677
  const accessor = markSignalAccessor((...args) => {
1379
1678
  if (args.length === 0) return s();
1380
- s(args[0]);
1679
+ s(resolveSignalSetter(() => s(), args[0]));
1381
1680
  });
1382
1681
  state[entry.key] = accessor;
1383
1682
  }
1384
1683
  return state;
1385
1684
  }
1685
+ function buildSsrActions() {
1686
+ const out = {};
1687
+ for (const entry of actions) {
1688
+ const invoke = () => {};
1689
+ Object.defineProperties(invoke, {
1690
+ pending: {
1691
+ get: () => false,
1692
+ enumerable: true
1693
+ },
1694
+ data: {
1695
+ get: () => void 0,
1696
+ enumerable: true
1697
+ },
1698
+ error: {
1699
+ get: () => void 0,
1700
+ enumerable: true
1701
+ }
1702
+ });
1703
+ out[entry.key] = invoke;
1704
+ }
1705
+ return out;
1706
+ }
1386
1707
  /** Detached host for SSR setup hooks; must not be `{}` (Areia/components call host.matches). */
1387
1708
  function createSsrOnMountHost() {
1388
1709
  if (typeof document !== "undefined") return document.createElement("div");
@@ -1398,18 +1719,19 @@ var IlhaBuilder = class IlhaBuilder {
1398
1719
  return stub;
1399
1720
  }
1400
1721
  /** Run .onMount() before top-level SSR only (seed module/external state from `input`). */
1401
- function runOnMountForRender(input, state, derived) {
1722
+ function runOnMountForRender({ input, state, derived, action }) {
1402
1723
  if (onMounts.length === 0) return;
1403
1724
  if (currentRenderCtx()) return;
1404
1725
  const host = createSsrOnMountHost();
1405
1726
  const ssrCleanups = [];
1406
1727
  for (const entry of onMounts) {
1407
1728
  const prevSub = setActiveSub(void 0);
1408
- let userCleanup = void 0;
1729
+ let userCleanup;
1409
1730
  try {
1410
1731
  userCleanup = entry.fn({
1411
1732
  state,
1412
1733
  derived,
1734
+ action,
1413
1735
  input,
1414
1736
  host,
1415
1737
  hydrated: false
@@ -1424,6 +1746,7 @@ var IlhaBuilder = class IlhaBuilder {
1424
1746
  source: "mount",
1425
1747
  state,
1426
1748
  derived,
1749
+ action,
1427
1750
  input,
1428
1751
  host
1429
1752
  });
@@ -1447,6 +1770,7 @@ var IlhaBuilder = class IlhaBuilder {
1447
1770
  function renderToString(props, sync = false) {
1448
1771
  const input = resolveInput(props);
1449
1772
  const state = buildSignalState(input);
1773
+ const action = buildSsrActions();
1450
1774
  const results = deriveds.map((entry) => {
1451
1775
  const prevSub = setActiveSub(void 0);
1452
1776
  try {
@@ -1480,14 +1804,20 @@ var IlhaBuilder = class IlhaBuilder {
1480
1804
  error: void 0
1481
1805
  };
1482
1806
  const derived = buildDerivedAccessors(envelopes);
1483
- if (!currentRenderCtx()) runOnMountForRender(input, state, derived);
1807
+ if (!currentRenderCtx()) runOnMountForRender({
1808
+ input,
1809
+ state,
1810
+ derived,
1811
+ action
1812
+ });
1484
1813
  const prevSub = setActiveSub(void 0);
1485
1814
  try {
1486
- const { html } = renderWithCtx(() => fn({
1815
+ const { html } = renderWithCtx({ render: () => fn({
1487
1816
  state,
1488
1817
  derived,
1818
+ action,
1489
1819
  input
1490
- }));
1820
+ }) });
1491
1821
  return stylePrefix + html;
1492
1822
  } finally {
1493
1823
  setActiveSub(prevSub);
@@ -1517,14 +1847,23 @@ var IlhaBuilder = class IlhaBuilder {
1517
1847
  const envelopes = {};
1518
1848
  for (const r of resolved) envelopes[r.key] = r.envelope;
1519
1849
  const derived = buildDerivedAccessors(envelopes);
1520
- if (!currentRenderCtx()) runOnMountForRender(input, state, derived);
1850
+ if (!currentRenderCtx()) runOnMountForRender({
1851
+ input,
1852
+ state,
1853
+ derived,
1854
+ action
1855
+ });
1521
1856
  const prevSub = setActiveSub(void 0);
1522
1857
  try {
1523
- const { html } = await renderWithCtx(() => fn({
1524
- state,
1525
- derived,
1526
- input
1527
- }), void 0, true);
1858
+ const { html } = await renderWithCtx({
1859
+ render: () => fn({
1860
+ state,
1861
+ derived,
1862
+ action,
1863
+ input
1864
+ }),
1865
+ asyncChildren: true
1866
+ });
1528
1867
  return stylePrefix + html;
1529
1868
  } finally {
1530
1869
  setActiveSub(prevSub);
@@ -1539,7 +1878,7 @@ var IlhaBuilder = class IlhaBuilder {
1539
1878
  const rawProps = host.getAttribute(PROPS_ATTR);
1540
1879
  if (rawProps) {
1541
1880
  const parsed = safeParseSnapshot(rawProps, PROPS_ATTR);
1542
- if (parsed !== void 0) props = parsed;
1881
+ if (parsed !== void 0) props = reviveSlotProps(parsed);
1543
1882
  }
1544
1883
  }
1545
1884
  const inputSignal = signal(resolveInput(props));
@@ -1591,8 +1930,14 @@ var IlhaBuilder = class IlhaBuilder {
1591
1930
  const cleanups = [];
1592
1931
  const unmountController = new AbortController();
1593
1932
  cleanups.push(() => unmountController.abort());
1594
- const { proxy: derived, setup: setupDerived } = createDerivedProxy(deriveds, state, input, derivedSnapshot);
1933
+ const { proxy: derived, setup: setupDerived } = createDerivedProxy({
1934
+ entries: deriveds,
1935
+ state,
1936
+ input,
1937
+ derivedSnapshot
1938
+ });
1595
1939
  let stopDerived = () => {};
1940
+ let action = {};
1596
1941
  function reportError(err, source) {
1597
1942
  const error = err instanceof Error ? err : new Error(String(err));
1598
1943
  if (onErrors.length === 0) {
@@ -1605,6 +1950,7 @@ var IlhaBuilder = class IlhaBuilder {
1605
1950
  source,
1606
1951
  state,
1607
1952
  derived,
1953
+ action,
1608
1954
  input,
1609
1955
  host
1610
1956
  });
@@ -1612,6 +1958,107 @@ var IlhaBuilder = class IlhaBuilder {
1612
1958
  console.error(handlerErr);
1613
1959
  }
1614
1960
  }
1961
+ const actionEntries = actions;
1962
+ const actionObject = {};
1963
+ let disposed = false;
1964
+ cleanups.push(() => {
1965
+ disposed = true;
1966
+ });
1967
+ for (const entry of actionEntries) {
1968
+ const envelope = signal({
1969
+ pending: 0,
1970
+ data: void 0,
1971
+ error: void 0
1972
+ });
1973
+ const readEnvelope = () => untrack(() => envelope());
1974
+ let latestRun = 0;
1975
+ const invoke = (props) => {
1976
+ if (currentRenderCtx()) return;
1977
+ if (disposed) return;
1978
+ const run = ++latestRun;
1979
+ const runSignal = unmountController.signal;
1980
+ const ctx = {
1981
+ state,
1982
+ derived,
1983
+ input,
1984
+ host,
1985
+ signal: runSignal
1986
+ };
1987
+ let result;
1988
+ let isAsync = false;
1989
+ startBatch();
1990
+ try {
1991
+ result = entry.fn(props, ctx);
1992
+ isAsync = result != null && typeof result.then === "function";
1993
+ const current = readEnvelope();
1994
+ envelope(isAsync ? {
1995
+ pending: current.pending + 1,
1996
+ data: current.data,
1997
+ error: void 0
1998
+ } : {
1999
+ pending: current.pending,
2000
+ data: result,
2001
+ error: void 0
2002
+ });
2003
+ } catch (error) {
2004
+ const normalized = error instanceof Error ? error : new Error(String(error));
2005
+ envelope({
2006
+ pending: readEnvelope().pending,
2007
+ data: void 0,
2008
+ error: normalized
2009
+ });
2010
+ reportError(normalized, "action");
2011
+ return;
2012
+ } finally {
2013
+ endBatch();
2014
+ }
2015
+ if (!isAsync) return;
2016
+ Promise.resolve(result).then((data) => {
2017
+ if (disposed || runSignal.aborted || run !== latestRun) return;
2018
+ const value = readEnvelope();
2019
+ envelope({
2020
+ pending: value.pending,
2021
+ data,
2022
+ error: void 0
2023
+ });
2024
+ }).catch((error) => {
2025
+ if (disposed || runSignal.aborted || error?.name === "AbortError") return;
2026
+ const normalized = error instanceof Error ? error : new Error(String(error));
2027
+ if (run === latestRun) {
2028
+ const value = readEnvelope();
2029
+ envelope({
2030
+ pending: value.pending,
2031
+ data: void 0,
2032
+ error: normalized
2033
+ });
2034
+ }
2035
+ reportError(normalized, "action");
2036
+ }).finally(() => {
2037
+ if (disposed) return;
2038
+ const value = readEnvelope();
2039
+ envelope({
2040
+ ...value,
2041
+ pending: Math.max(0, value.pending - 1)
2042
+ });
2043
+ });
2044
+ };
2045
+ Object.defineProperties(invoke, {
2046
+ pending: {
2047
+ get: () => envelope().pending > 0,
2048
+ enumerable: true
2049
+ },
2050
+ data: {
2051
+ get: () => envelope().data,
2052
+ enumerable: true
2053
+ },
2054
+ error: {
2055
+ get: () => envelope().error,
2056
+ enumerable: true
2057
+ }
2058
+ });
2059
+ actionObject[entry.key] = invoke;
2060
+ }
2061
+ action = actionObject;
1615
2062
  if (transition?.enter) try {
1616
2063
  const result = transition.enter(host);
1617
2064
  if (result instanceof Promise) result.catch((err) => reportError(err, "transition"));
@@ -1661,6 +2108,13 @@ var IlhaBuilder = class IlhaBuilder {
1661
2108
  }
1662
2109
  return index;
1663
2110
  }
2111
+ /** Push live props from a fresh slot map into already-mounted children. */
2112
+ function pushUpdatedProps(nextSlots) {
2113
+ for (const [id, entry] of mountedSlots) {
2114
+ const next = nextSlots.get(id);
2115
+ if (next) entry.updateProps(next.props);
2116
+ }
2117
+ }
1664
2118
  function mountSlots(slotMap) {
1665
2119
  for (const [id, entry] of mountedSlots) if (!slotMap.has(id)) teardownMountedSlot(id, entry);
1666
2120
  for (const [id, entry] of mountedSlots) {
@@ -1682,7 +2136,7 @@ var IlhaBuilder = class IlhaBuilder {
1682
2136
  const rawProps = slotEl.getAttribute(PROPS_ATTR) ?? slotEl.getAttribute("data-props");
1683
2137
  if (rawProps) {
1684
2138
  const parsed = safeParseSnapshot(rawProps, `props on [${SLOT_ATTR}="${id}"]`);
1685
- if (parsed !== void 0) slotProps = parsed;
2139
+ if (parsed !== void 0) slotProps = reviveSlotProps(parsed);
1686
2140
  }
1687
2141
  }
1688
2142
  const prevSub = setActiveSub(void 0);
@@ -1709,7 +2163,13 @@ var IlhaBuilder = class IlhaBuilder {
1709
2163
  function attachListeners() {
1710
2164
  for (const entry of ons) {
1711
2165
  if (entry.options.once && firedOnce.has(entry)) continue;
1712
- (entry.selector === "" ? [host] : Array.from(host.querySelectorAll(entry.selector))).forEach((listenerTarget) => {
2166
+ let targets;
2167
+ try {
2168
+ targets = entry.selector === "" ? [host] : Array.from(host.querySelectorAll(entry.selector)).filter((element) => templateElementBelongsToHost(host, element));
2169
+ } catch {
2170
+ continue;
2171
+ }
2172
+ targets.forEach((listenerTarget) => {
1713
2173
  const listener = (event) => {
1714
2174
  if (entry.options.once) {
1715
2175
  firedOnce.add(entry);
@@ -1737,6 +2197,7 @@ var IlhaBuilder = class IlhaBuilder {
1737
2197
  result = entry.handler({
1738
2198
  state,
1739
2199
  derived,
2200
+ action,
1740
2201
  input,
1741
2202
  host,
1742
2203
  target: handlerTarget,
@@ -1776,11 +2237,12 @@ var IlhaBuilder = class IlhaBuilder {
1776
2237
  function invokeOnMounts() {
1777
2238
  for (const entry of onMounts) {
1778
2239
  const prevSub = setActiveSub(void 0);
1779
- let userCleanup = void 0;
2240
+ let userCleanup;
1780
2241
  try {
1781
2242
  userCleanup = entry.fn({
1782
2243
  state,
1783
2244
  derived,
2245
+ action,
1784
2246
  input,
1785
2247
  host,
1786
2248
  hydrated
@@ -1804,14 +2266,27 @@ var IlhaBuilder = class IlhaBuilder {
1804
2266
  }
1805
2267
  const preserveSSRDom = hydrated && host.childNodes.length > 0;
1806
2268
  if (hydrated && onMounts.length > 0 && !shouldSkipOnMount) invokeOnMounts();
1807
- const initial = renderWithCtx(() => fn({
1808
- state,
1809
- derived,
1810
- input
1811
- }), preserveSSRDom ? host : void 0);
2269
+ const nativeOnceFired = /* @__PURE__ */ new WeakMap();
2270
+ const initial = renderWithCtx({
2271
+ render: () => fn({
2272
+ state,
2273
+ derived,
2274
+ action,
2275
+ input
2276
+ }),
2277
+ liveHost: preserveSSRDom ? host : void 0
2278
+ });
1812
2279
  if (!preserveSSRDom) host.innerHTML = stylePrefix + initial.html;
1813
2280
  let stopBindings = applyTemplateBindings(host, initial.binds);
1814
2281
  cleanups.push(() => stopBindings());
2282
+ let stopJsxEvents = applyJsxEvents({
2283
+ host,
2284
+ records: initial.events,
2285
+ reportError: (error) => reportError(error, "on"),
2286
+ unmountSignal: unmountController.signal,
2287
+ onceFired: nativeOnceFired
2288
+ });
2289
+ cleanups.push(() => stopJsxEvents());
1815
2290
  mountSlots(initial.slots);
1816
2291
  cleanups.push(() => mountedSlots.forEach((entry) => entry.unmount()));
1817
2292
  attachListeners();
@@ -1836,6 +2311,7 @@ var IlhaBuilder = class IlhaBuilder {
1836
2311
  userCleanup = entry.fn({
1837
2312
  state,
1838
2313
  derived,
2314
+ action,
1839
2315
  input,
1840
2316
  host,
1841
2317
  signal: runSignal
@@ -1864,19 +2340,28 @@ var IlhaBuilder = class IlhaBuilder {
1864
2340
  let renderEpoch = 0;
1865
2341
  const stopRender = effect(() => {
1866
2342
  const epoch = ++renderEpoch;
1867
- const { html: rendered, slots: newSlotMap, binds: newBinds } = renderWithCtx(() => fn({
1868
- state,
1869
- derived,
1870
- input
1871
- }), host);
2343
+ const { html: rendered, slots: newSlotMap, binds: newBinds, events: newEvents } = renderWithCtx({
2344
+ render: () => fn({
2345
+ state,
2346
+ derived,
2347
+ action,
2348
+ input
2349
+ }),
2350
+ liveHost: host
2351
+ });
2352
+ stopJsxEvents();
2353
+ stopJsxEvents = applyJsxEvents({
2354
+ host,
2355
+ records: newEvents,
2356
+ reportError: (error) => reportError(error, "on"),
2357
+ unmountSignal: unmountController.signal,
2358
+ onceFired: nativeOnceFired
2359
+ });
1872
2360
  const html = stylePrefix + rendered;
1873
2361
  if (!initialized) {
1874
2362
  initialized = true;
1875
2363
  if (preserveSSRDom) {
1876
- for (const [id, entry] of mountedSlots) {
1877
- const next = newSlotMap.get(id);
1878
- if (next) entry.updateProps(next.props);
1879
- }
2364
+ pushUpdatedProps(newSlotMap);
1880
2365
  if (rendered === initialRenderedHtml) {
1881
2366
  lastRendered = rendered;
1882
2367
  return;
@@ -1886,17 +2371,22 @@ var IlhaBuilder = class IlhaBuilder {
1886
2371
  lastRendered = rendered;
1887
2372
  return;
1888
2373
  }
1889
- if (mountedSlots.size > 0 && mountedSlots.size === newSlotMap.size && [...newSlotMap.keys()].every((id) => mountedSlots.has(id)) && isStableInlineSlotMount(initialRenderedHtml, rendered, newSlotMap.keys())) return;
2374
+ if (mountedSlots.size > 0 && mountedSlots.size === newSlotMap.size && [...newSlotMap.keys()].every((id) => mountedSlots.has(id)) && isStableInlineSlotMount({
2375
+ initialHtml: initialRenderedHtml,
2376
+ renderedHtml: rendered,
2377
+ slotIds: newSlotMap.keys()
2378
+ })) {
2379
+ pushUpdatedProps(newSlotMap);
2380
+ return;
2381
+ }
1890
2382
  }
1891
2383
  if (rendered === lastRendered && mountedSlots.size === newSlotMap.size && [...newSlotMap].every(([id, next]) => mountedSlots.get(id)?.island === next.island)) {
1892
- for (const [id, entry] of mountedSlots) {
1893
- const next = newSlotMap.get(id);
1894
- if (next) entry.updateProps(next.props);
1895
- }
2384
+ pushUpdatedProps(newSlotMap);
1896
2385
  return;
1897
2386
  }
1898
2387
  detachListeners();
1899
2388
  stopBindings();
2389
+ stopJsxEvents();
1900
2390
  const prevMountedCount = mountedSlots.size;
1901
2391
  const leavingPromises = [];
1902
2392
  for (const [id, entry] of mountedSlots) if (!newSlotMap.has(id)) {
@@ -1942,6 +2432,13 @@ var IlhaBuilder = class IlhaBuilder {
1942
2432
  }
1943
2433
  attachListeners();
1944
2434
  stopBindings = applyTemplateBindings(host, newBinds);
2435
+ stopJsxEvents = applyJsxEvents({
2436
+ host,
2437
+ records: newEvents,
2438
+ reportError: (error) => reportError(error, "on"),
2439
+ unmountSignal: unmountController.signal,
2440
+ onceFired: nativeOnceFired
2441
+ });
1945
2442
  mountSlots(newSlotMap);
1946
2443
  };
1947
2444
  if (leavingPromises.length > 0) Promise.allSettled(leavingPromises).then((results) => {
@@ -1954,6 +2451,8 @@ var IlhaBuilder = class IlhaBuilder {
1954
2451
  const unmount = () => {
1955
2452
  if (tornDown) return;
1956
2453
  tornDown = true;
2454
+ disposed = true;
2455
+ unmountController.abort();
1957
2456
  ISLAND_MOUNT_HANDLES.delete(host);
1958
2457
  stopRender();
1959
2458
  detachListeners();
@@ -2001,7 +2500,7 @@ var IlhaBuilder = class IlhaBuilder {
2001
2500
  * The declared return type omits {@link IslandCall} because TypeScript
2002
2501
  * cannot narrow based on runtime stack state.
2003
2502
  */
2004
- const island = function(props) {
2503
+ const island = ((props) => {
2005
2504
  if (currentRenderCtx()) return {
2006
2505
  [ISLAND_CALL]: true,
2007
2506
  island,
@@ -2009,7 +2508,7 @@ var IlhaBuilder = class IlhaBuilder {
2009
2508
  key: void 0
2010
2509
  };
2011
2510
  return renderToString(props);
2012
- };
2511
+ });
2013
2512
  island.toString = (props) => renderToString(props, true);
2014
2513
  island.mount = (host, props) => mountIsland(host, props);
2015
2514
  island[ISLAND_MOUNT_INTERNAL] = (host, props) => mountIslandInternal(host, props);
@@ -2198,7 +2697,7 @@ function mountAll(registry, options = {}) {
2198
2697
  const rawProps = host.getAttribute(PROPS_ATTR);
2199
2698
  if (rawProps) {
2200
2699
  const parsed = safeParseSnapshot(rawProps, `${PROPS_ATTR} on [data-ilha="${name}"]`);
2201
- if (parsed !== void 0) props = parsed;
2700
+ if (parsed !== void 0) props = reviveSlotProps(parsed);
2202
2701
  }
2203
2702
  unmounts.push(island.mount(host, props));
2204
2703
  }
@@ -2232,6 +2731,7 @@ const rootBuilder = new IlhaBuilder({
2232
2731
  defaultInput: null,
2233
2732
  states: [],
2234
2733
  deriveds: [],
2734
+ actions: [],
2235
2735
  ons: [],
2236
2736
  effects: [],
2237
2737
  onMounts: [],
@@ -2240,7 +2740,17 @@ const rootBuilder = new IlhaBuilder({
2240
2740
  css: null,
2241
2741
  as: null
2242
2742
  });
2243
- const ilha = Object.assign(rootBuilder, {
2743
+ const callableRoot = ((fn) => rootBuilder.input().render(fn));
2744
+ Object.defineProperty(callableRoot, "_cfg", {
2745
+ value: rootBuilder._cfg,
2746
+ enumerable: false
2747
+ });
2748
+ for (const key of Object.getOwnPropertyNames(IlhaBuilder.prototype)) {
2749
+ if (key === "constructor") continue;
2750
+ const descriptor = Object.getOwnPropertyDescriptor(IlhaBuilder.prototype, key);
2751
+ if (descriptor) Object.defineProperty(callableRoot, key, descriptor);
2752
+ }
2753
+ const ilha = Object.assign(callableRoot, {
2244
2754
  html: ilhaHtml,
2245
2755
  raw: ilhaRaw,
2246
2756
  mount: mountAll,
@@ -2252,9 +2762,25 @@ const ilha = Object.assign(rootBuilder, {
2252
2762
  untrack,
2253
2763
  onUncaughtError
2254
2764
  });
2765
+ /** @internal Used by the separate JSX runtime to register a native event handler. */
2766
+ function __ilhaJsxEvent({ type, handler, modifier }) {
2767
+ const ctx = currentRenderCtx();
2768
+ if (!ctx) return void 0;
2769
+ const index = ctx.events.length;
2770
+ ctx.events.push({
2771
+ type,
2772
+ handler,
2773
+ modifier
2774
+ });
2775
+ return index;
2776
+ }
2255
2777
  /** @internal Used by the separate JSX runtime entry to preserve island slot composition. */
2256
- function __ilhaJsxSlot(island, props, key) {
2257
- return ilhaRaw(emitIslandSlot(island, props, key));
2778
+ function __ilhaJsxSlot({ island, props, key }) {
2779
+ return ilhaRaw(emitIslandSlot({
2780
+ island,
2781
+ props,
2782
+ key
2783
+ }));
2258
2784
  }
2259
2785
  const html = ilhaHtml;
2260
2786
  const raw = ilhaRaw;
@@ -2264,4 +2790,4 @@ const from = ilhaFrom;
2264
2790
  const context = ilhaContext;
2265
2791
 
2266
2792
  //#endregion
2267
- export { ISLAND_MOUNT_HANDLES, ISLAND_MOUNT_INTERNAL, __ilhaJsxSlot, batch, ilhaComputed as computed, context, css, ilha as default, ilhaEffect as effect, from, html, ilhaSignal, ilhaSignal as signal, mount, onUncaughtError, raw, untrack };
2793
+ export { ISLAND_MOUNT_HANDLES, ISLAND_MOUNT_INTERNAL, __ilhaJsxEvent, __ilhaJsxSlot, batch, ilhaComputed as computed, context, css, ilha as default, ilhaEffect as effect, from, html, ilhaSignal, ilhaSignal as signal, mount, onUncaughtError, raw, untrack };