ilha 0.9.5 → 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);
@@ -485,7 +485,8 @@ const PROPS_ATTR = "data-ilha-props";
485
485
  const STATE_ATTR = "data-ilha-state";
486
486
  const CSS_ATTR = "data-ilha-css";
487
487
  function buildScopedStyle(css) {
488
- 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>`;
489
490
  }
490
491
  const RENDER_CTX_STACK = Symbol.for("ilha.renderCtxStack");
491
492
  function renderCtxStack() {
@@ -498,7 +499,8 @@ function pushRenderCtx(liveHost, asyncChildren) {
498
499
  positional: 0,
499
500
  liveHost,
500
501
  pending: asyncChildren ? /* @__PURE__ */ new Map() : void 0,
501
- binds: []
502
+ binds: [],
503
+ events: []
502
504
  };
503
505
  renderCtxStack().push(ctx);
504
506
  return ctx;
@@ -518,7 +520,7 @@ function isIslandCall(v) {
518
520
  if (ISLAND_CALL in v) return true;
519
521
  return typeof v === "object" && "island" in v && isIsland(v.island);
520
522
  }
521
- function emitIslandSlot(island, props, key) {
523
+ function emitIslandSlot({ island, props, key }) {
522
524
  const ctx = currentRenderCtx();
523
525
  let id;
524
526
  if (key !== void 0) id = `k:${key}`;
@@ -530,22 +532,42 @@ function emitIslandSlot(island, props, key) {
530
532
  const slotTag = getIslandSlotTag(island);
531
533
  const attrProps = slotPropsForAttr(props);
532
534
  const propsAttr = attrProps ? ` ${PROPS_ATTR}='${escapeHtml(JSON.stringify(attrProps))}'` : "";
533
- if (ctx?.liveHost) return wrapIslandSlotHtml(slotTag, id, propsAttr, "");
535
+ if (ctx?.liveHost) return wrapIslandSlotHtml({
536
+ tag: slotTag,
537
+ id,
538
+ propsAttr,
539
+ inner: ""
540
+ });
534
541
  if (ctx?.pending) {
535
542
  popRenderCtx();
536
543
  try {
537
544
  const result = island(props);
538
545
  if (result instanceof Promise) {
539
546
  ctx.pending.set(id, result.then(String));
540
- return wrapIslandSlotHtml(slotTag, id, propsAttr, asyncSlotMarker(id));
547
+ return wrapIslandSlotHtml({
548
+ tag: slotTag,
549
+ id,
550
+ propsAttr,
551
+ inner: asyncSlotMarker(id)
552
+ });
541
553
  }
542
- return wrapIslandSlotHtml(slotTag, id, propsAttr, String(result));
554
+ return wrapIslandSlotHtml({
555
+ tag: slotTag,
556
+ id,
557
+ propsAttr,
558
+ inner: String(result)
559
+ });
543
560
  } finally {
544
561
  renderCtxStack().push(ctx);
545
562
  }
546
563
  }
547
564
  const inner = island.toString(props);
548
- return wrapIslandSlotHtml(slotTag, id, propsAttr, inner);
565
+ return wrapIslandSlotHtml({
566
+ tag: slotTag,
567
+ id,
568
+ propsAttr,
569
+ inner
570
+ });
549
571
  }
550
572
  function asyncSlotMarker(id) {
551
573
  return `<!--ilha-async:${escapeHtml(id)}-->`;
@@ -557,6 +579,15 @@ async function resolveAsyncChildren(html, pending) {
557
579
  }
558
580
  return html;
559
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
+ }
560
591
  function markSignalAccessor(fn) {
561
592
  fn[SIGNAL_ACCESSOR] = true;
562
593
  const accessor = fn;
@@ -574,14 +605,19 @@ function getAtPath(obj, path) {
574
605
  }
575
606
  return cur;
576
607
  }
577
- function setAtPath(obj, path, value) {
608
+ function setAtPath({ object, path, value }) {
609
+ const obj = object;
578
610
  if (path.length === 0) return value;
579
611
  const [head, ...rest] = path;
580
612
  if (Array.isArray(obj)) {
581
613
  const idx = head;
582
614
  if (idx < 0 || idx >= obj.length) return obj;
583
615
  const next = obj[idx];
584
- 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
+ });
585
621
  if (Object.is(next, updated)) return obj;
586
622
  const copy = obj.slice();
587
623
  copy[idx] = updated;
@@ -591,7 +627,11 @@ function setAtPath(obj, path, value) {
591
627
  const record = obj;
592
628
  const key = String(head);
593
629
  const next = record[key];
594
- 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
+ });
595
635
  if (rest.length === 0) {
596
636
  if (Object.is(next, value)) return obj;
597
637
  } else if (Object.is(next, updated)) return obj;
@@ -600,7 +640,11 @@ function setAtPath(obj, path, value) {
600
640
  [key]: updated
601
641
  };
602
642
  }
603
- 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
+ });
604
648
  }
605
649
  function toPathSegment(prop) {
606
650
  if (typeof prop === "symbol") return null;
@@ -629,8 +673,15 @@ function createSelectAccessor(root, selector) {
629
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.");
630
674
  return markSignalAccessor((...args) => {
631
675
  if (args.length === 0) return path.length === 0 ? selector(root()) : getAtPath(root(), path);
632
- const next = path.length === 0 ? args[0] : setAtPath(root(), path, args[0]);
633
- 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));
634
685
  });
635
686
  }
636
687
  function ilhaRaw(value) {
@@ -654,9 +705,17 @@ function interpolateValue(v) {
654
705
  if (isRawHtml(v)) return v.value;
655
706
  if (isIslandCall(v)) {
656
707
  const call = typeof v === "function" ? v() : v;
657
- return emitIslandSlot(call.island, call.props, call.key);
708
+ return emitIslandSlot({
709
+ island: call.island,
710
+ props: call.props,
711
+ key: call.key
712
+ });
658
713
  }
659
- 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
+ });
660
719
  if (isSignalAccessor(v)) return escapeHtml(v());
661
720
  if (typeof v === "function") return escapeHtml(v());
662
721
  return escapeHtml(v);
@@ -672,6 +731,30 @@ const BIND_VALID_KINDS = /* @__PURE__ */ new Set([
672
731
  "this"
673
732
  ]);
674
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
+ }
675
758
  function findTagCloseIndex(chunk, initialQuote) {
676
759
  let quote = initialQuote;
677
760
  for (let i = 0; i < chunk.length; i++) {
@@ -706,7 +789,7 @@ function extractElementStaticValue(prefix) {
706
789
  if (!m) return null;
707
790
  return m[2] ?? m[3] ?? m[4] ?? null;
708
791
  }
709
- function emitBindSSR(kind, index, accessor, prefixForStaticPeek) {
792
+ function emitBindSSR({ kind, index, accessor, prefixForStaticPeek }) {
710
793
  const spec = `${kind}:${index}`;
711
794
  let v;
712
795
  try {
@@ -733,6 +816,7 @@ function ilhaHtml(strings, ...values) {
733
816
  let result = "";
734
817
  let stripLeadingQuote = null;
735
818
  let pendingBindSpecs = "";
819
+ let pendingEventSpecs = "";
736
820
  let pendingQuote = null;
737
821
  const ctx = currentRenderCtx();
738
822
  for (let i = 0; i < strings.length; i++) {
@@ -741,11 +825,15 @@ function ilhaHtml(strings, ...values) {
741
825
  if (chunk.startsWith(stripLeadingQuote)) chunk = chunk.slice(1);
742
826
  stripLeadingQuote = null;
743
827
  }
744
- if (pendingBindSpecs !== "") {
828
+ if (pendingBindSpecs !== "" || pendingEventSpecs !== "") {
745
829
  const { index: gtIdx, quote } = findTagCloseIndex(chunk, pendingQuote);
746
830
  if (gtIdx !== -1) {
747
- 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);
748
835
  pendingBindSpecs = "";
836
+ pendingEventSpecs = "";
749
837
  pendingQuote = null;
750
838
  } else pendingQuote = quote;
751
839
  }
@@ -754,6 +842,29 @@ function ilhaHtml(strings, ...values) {
754
842
  continue;
755
843
  }
756
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
+ }
757
868
  const m = chunk.match(BIND_PREFIX_RE);
758
869
  if (m && isSignalAccessor(value)) {
759
870
  const name = m[1];
@@ -776,7 +887,12 @@ function ilhaHtml(strings, ...values) {
776
887
  kind: name,
777
888
  accessor: value
778
889
  });
779
- 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
+ });
780
896
  result += cleanPrefix + valueAttrs;
781
897
  pendingBindSpecs += (pendingBindSpecs ? "," : "") + spec;
782
898
  if (openQuote) stripLeadingQuote = openQuote;
@@ -785,6 +901,7 @@ function ilhaHtml(strings, ...values) {
785
901
  result += chunk + interpolateValue(value);
786
902
  }
787
903
  if (pendingBindSpecs !== "") result += ` data-ilha-bind="${pendingBindSpecs}"`;
904
+ if (pendingEventSpecs !== "") result += ` data-ilha-on="${pendingEventSpecs}"`;
788
905
  return {
789
906
  [RAW]: true,
790
907
  value: dedentString(result)
@@ -803,7 +920,7 @@ function ilhaContextFn(key, initial) {
803
920
  const s = signal(initial);
804
921
  const accessor = (...args) => {
805
922
  if (args.length === 0) return s();
806
- s(args[0]);
923
+ s(resolveSignalSetter(() => s(), args[0]));
807
924
  };
808
925
  contextRegistry.set(key, accessor);
809
926
  return accessor;
@@ -833,7 +950,7 @@ function ilhaSignal(initial) {
833
950
  const s = signal(initial);
834
951
  return markSignalAccessor((...args) => {
835
952
  if (args.length === 0) return s();
836
- s(args[0]);
953
+ s(resolveSignalSetter(() => s(), args[0]));
837
954
  });
838
955
  }
839
956
  /**
@@ -960,7 +1077,7 @@ function buildDerivedAccessors(envelopes) {
960
1077
  return accessors.get(key) ?? defaultDerivedAccessor();
961
1078
  } });
962
1079
  }
963
- function createDerivedProxy(entries, state, input, derivedSnapshot) {
1080
+ function createDerivedProxy({ entries, state, input, derivedSnapshot }) {
964
1081
  const envelopes = /* @__PURE__ */ new Map();
965
1082
  for (const entry of entries) {
966
1083
  let initialEnvelope;
@@ -1067,9 +1184,10 @@ function createDerivedProxy(entries, state, input, derivedSnapshot) {
1067
1184
  return;
1068
1185
  }
1069
1186
  const prevSub = setActiveSub(void 0);
1187
+ const prevVal = env();
1070
1188
  env({
1071
1189
  loading: true,
1072
- value: env().value,
1190
+ value: prevVal.value,
1073
1191
  error: void 0
1074
1192
  });
1075
1193
  setActiveSub(prevSub);
@@ -1102,6 +1220,7 @@ function createDerivedProxy(entries, state, input, derivedSnapshot) {
1102
1220
  };
1103
1221
  }
1104
1222
  const BIND_SENTINEL_ATTR = "data-ilha-bind";
1223
+ const EVENT_SENTINEL_ATTR = "data-ilha-on";
1105
1224
  function parseBindSentinel(value) {
1106
1225
  const out = [];
1107
1226
  for (const part of value.split(",")) {
@@ -1181,12 +1300,86 @@ function resolveBindOps(el, kind) {
1181
1300
  }
1182
1301
  }
1183
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
+ }
1184
1377
  function applyTemplateBindings(host, binds) {
1185
1378
  if (binds.length === 0) return () => {};
1186
1379
  const cleanups = [];
1187
1380
  const elements = [];
1188
1381
  if (host.hasAttribute(BIND_SENTINEL_ATTR)) elements.push(host);
1189
- 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);
1190
1383
  for (const el of elements) {
1191
1384
  const specs = parseBindSentinel(el.getAttribute(BIND_SENTINEL_ATTR));
1192
1385
  for (const spec of specs) {
@@ -1328,6 +1521,7 @@ var IlhaBuilder = class IlhaBuilder {
1328
1521
  defaultInput,
1329
1522
  states: [],
1330
1523
  deriveds: [],
1524
+ actions: [],
1331
1525
  ons: [],
1332
1526
  effects: [],
1333
1527
  onMounts: [],
@@ -1363,6 +1557,15 @@ var IlhaBuilder = class IlhaBuilder {
1363
1557
  }]
1364
1558
  });
1365
1559
  }
1560
+ action(key, fn) {
1561
+ return new IlhaBuilder({
1562
+ ...this._cfg,
1563
+ actions: [...this._cfg.actions, {
1564
+ key,
1565
+ fn
1566
+ }]
1567
+ });
1568
+ }
1366
1569
  on(selectorOrCombined, handler) {
1367
1570
  const parsed = parseOnArgs(selectorOrCombined);
1368
1571
  return new IlhaBuilder({
@@ -1417,7 +1620,7 @@ var IlhaBuilder = class IlhaBuilder {
1417
1620
  });
1418
1621
  }
1419
1622
  render(fn) {
1420
- 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;
1421
1624
  const stylePrefix = cssSource != null ? buildScopedStyle(cssSource) : "";
1422
1625
  const configuredSlotTag = slotAs ?? "div";
1423
1626
  function resolveInput(props) {
@@ -1428,26 +1631,29 @@ var IlhaBuilder = class IlhaBuilder {
1428
1631
  if (!schema) return merged;
1429
1632
  return validateSchema(schema, merged);
1430
1633
  }
1431
- function renderWithCtx(fn, liveHost, asyncChildren) {
1634
+ function renderWithCtx({ render, liveHost, asyncChildren }) {
1432
1635
  const ctx = pushRenderCtx(liveHost, asyncChildren);
1433
1636
  try {
1434
- const html = unwrapHtml(fn());
1637
+ const html = unwrapHtml(render());
1435
1638
  const slots = ctx.slots;
1436
1639
  const binds = ctx.binds;
1640
+ const events = ctx.events;
1437
1641
  if (ctx.pending && ctx.pending.size > 0) {
1438
1642
  const pending = ctx.pending;
1439
1643
  return (async () => {
1440
1644
  return {
1441
1645
  html: await resolveAsyncChildren(html, pending),
1442
1646
  slots,
1443
- binds
1647
+ binds,
1648
+ events
1444
1649
  };
1445
1650
  })();
1446
1651
  }
1447
1652
  return {
1448
1653
  html,
1449
1654
  slots,
1450
- binds
1655
+ binds,
1656
+ events
1451
1657
  };
1452
1658
  } finally {
1453
1659
  popRenderCtx();
@@ -1470,12 +1676,34 @@ var IlhaBuilder = class IlhaBuilder {
1470
1676
  const s = signal(snapshot && entry.key in snapshot ? snapshot[entry.key] : typeof entry.init === "function" ? entry.init(input) : entry.init);
1471
1677
  const accessor = markSignalAccessor((...args) => {
1472
1678
  if (args.length === 0) return s();
1473
- s(args[0]);
1679
+ s(resolveSignalSetter(() => s(), args[0]));
1474
1680
  });
1475
1681
  state[entry.key] = accessor;
1476
1682
  }
1477
1683
  return state;
1478
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
+ }
1479
1707
  /** Detached host for SSR setup hooks; must not be `{}` (Areia/components call host.matches). */
1480
1708
  function createSsrOnMountHost() {
1481
1709
  if (typeof document !== "undefined") return document.createElement("div");
@@ -1491,18 +1719,19 @@ var IlhaBuilder = class IlhaBuilder {
1491
1719
  return stub;
1492
1720
  }
1493
1721
  /** Run .onMount() before top-level SSR only (seed module/external state from `input`). */
1494
- function runOnMountForRender(input, state, derived) {
1722
+ function runOnMountForRender({ input, state, derived, action }) {
1495
1723
  if (onMounts.length === 0) return;
1496
1724
  if (currentRenderCtx()) return;
1497
1725
  const host = createSsrOnMountHost();
1498
1726
  const ssrCleanups = [];
1499
1727
  for (const entry of onMounts) {
1500
1728
  const prevSub = setActiveSub(void 0);
1501
- let userCleanup = void 0;
1729
+ let userCleanup;
1502
1730
  try {
1503
1731
  userCleanup = entry.fn({
1504
1732
  state,
1505
1733
  derived,
1734
+ action,
1506
1735
  input,
1507
1736
  host,
1508
1737
  hydrated: false
@@ -1517,6 +1746,7 @@ var IlhaBuilder = class IlhaBuilder {
1517
1746
  source: "mount",
1518
1747
  state,
1519
1748
  derived,
1749
+ action,
1520
1750
  input,
1521
1751
  host
1522
1752
  });
@@ -1540,6 +1770,7 @@ var IlhaBuilder = class IlhaBuilder {
1540
1770
  function renderToString(props, sync = false) {
1541
1771
  const input = resolveInput(props);
1542
1772
  const state = buildSignalState(input);
1773
+ const action = buildSsrActions();
1543
1774
  const results = deriveds.map((entry) => {
1544
1775
  const prevSub = setActiveSub(void 0);
1545
1776
  try {
@@ -1573,14 +1804,20 @@ var IlhaBuilder = class IlhaBuilder {
1573
1804
  error: void 0
1574
1805
  };
1575
1806
  const derived = buildDerivedAccessors(envelopes);
1576
- if (!currentRenderCtx()) runOnMountForRender(input, state, derived);
1807
+ if (!currentRenderCtx()) runOnMountForRender({
1808
+ input,
1809
+ state,
1810
+ derived,
1811
+ action
1812
+ });
1577
1813
  const prevSub = setActiveSub(void 0);
1578
1814
  try {
1579
- const { html } = renderWithCtx(() => fn({
1815
+ const { html } = renderWithCtx({ render: () => fn({
1580
1816
  state,
1581
1817
  derived,
1818
+ action,
1582
1819
  input
1583
- }));
1820
+ }) });
1584
1821
  return stylePrefix + html;
1585
1822
  } finally {
1586
1823
  setActiveSub(prevSub);
@@ -1610,14 +1847,23 @@ var IlhaBuilder = class IlhaBuilder {
1610
1847
  const envelopes = {};
1611
1848
  for (const r of resolved) envelopes[r.key] = r.envelope;
1612
1849
  const derived = buildDerivedAccessors(envelopes);
1613
- if (!currentRenderCtx()) runOnMountForRender(input, state, derived);
1850
+ if (!currentRenderCtx()) runOnMountForRender({
1851
+ input,
1852
+ state,
1853
+ derived,
1854
+ action
1855
+ });
1614
1856
  const prevSub = setActiveSub(void 0);
1615
1857
  try {
1616
- const { html } = await renderWithCtx(() => fn({
1617
- state,
1618
- derived,
1619
- input
1620
- }), void 0, true);
1858
+ const { html } = await renderWithCtx({
1859
+ render: () => fn({
1860
+ state,
1861
+ derived,
1862
+ action,
1863
+ input
1864
+ }),
1865
+ asyncChildren: true
1866
+ });
1621
1867
  return stylePrefix + html;
1622
1868
  } finally {
1623
1869
  setActiveSub(prevSub);
@@ -1684,8 +1930,14 @@ var IlhaBuilder = class IlhaBuilder {
1684
1930
  const cleanups = [];
1685
1931
  const unmountController = new AbortController();
1686
1932
  cleanups.push(() => unmountController.abort());
1687
- 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
+ });
1688
1939
  let stopDerived = () => {};
1940
+ let action = {};
1689
1941
  function reportError(err, source) {
1690
1942
  const error = err instanceof Error ? err : new Error(String(err));
1691
1943
  if (onErrors.length === 0) {
@@ -1698,6 +1950,7 @@ var IlhaBuilder = class IlhaBuilder {
1698
1950
  source,
1699
1951
  state,
1700
1952
  derived,
1953
+ action,
1701
1954
  input,
1702
1955
  host
1703
1956
  });
@@ -1705,6 +1958,107 @@ var IlhaBuilder = class IlhaBuilder {
1705
1958
  console.error(handlerErr);
1706
1959
  }
1707
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;
1708
2062
  if (transition?.enter) try {
1709
2063
  const result = transition.enter(host);
1710
2064
  if (result instanceof Promise) result.catch((err) => reportError(err, "transition"));
@@ -1809,7 +2163,13 @@ var IlhaBuilder = class IlhaBuilder {
1809
2163
  function attachListeners() {
1810
2164
  for (const entry of ons) {
1811
2165
  if (entry.options.once && firedOnce.has(entry)) continue;
1812
- (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) => {
1813
2173
  const listener = (event) => {
1814
2174
  if (entry.options.once) {
1815
2175
  firedOnce.add(entry);
@@ -1837,6 +2197,7 @@ var IlhaBuilder = class IlhaBuilder {
1837
2197
  result = entry.handler({
1838
2198
  state,
1839
2199
  derived,
2200
+ action,
1840
2201
  input,
1841
2202
  host,
1842
2203
  target: handlerTarget,
@@ -1876,11 +2237,12 @@ var IlhaBuilder = class IlhaBuilder {
1876
2237
  function invokeOnMounts() {
1877
2238
  for (const entry of onMounts) {
1878
2239
  const prevSub = setActiveSub(void 0);
1879
- let userCleanup = void 0;
2240
+ let userCleanup;
1880
2241
  try {
1881
2242
  userCleanup = entry.fn({
1882
2243
  state,
1883
2244
  derived,
2245
+ action,
1884
2246
  input,
1885
2247
  host,
1886
2248
  hydrated
@@ -1904,14 +2266,27 @@ var IlhaBuilder = class IlhaBuilder {
1904
2266
  }
1905
2267
  const preserveSSRDom = hydrated && host.childNodes.length > 0;
1906
2268
  if (hydrated && onMounts.length > 0 && !shouldSkipOnMount) invokeOnMounts();
1907
- const initial = renderWithCtx(() => fn({
1908
- state,
1909
- derived,
1910
- input
1911
- }), 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
+ });
1912
2279
  if (!preserveSSRDom) host.innerHTML = stylePrefix + initial.html;
1913
2280
  let stopBindings = applyTemplateBindings(host, initial.binds);
1914
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());
1915
2290
  mountSlots(initial.slots);
1916
2291
  cleanups.push(() => mountedSlots.forEach((entry) => entry.unmount()));
1917
2292
  attachListeners();
@@ -1936,6 +2311,7 @@ var IlhaBuilder = class IlhaBuilder {
1936
2311
  userCleanup = entry.fn({
1937
2312
  state,
1938
2313
  derived,
2314
+ action,
1939
2315
  input,
1940
2316
  host,
1941
2317
  signal: runSignal
@@ -1964,11 +2340,23 @@ var IlhaBuilder = class IlhaBuilder {
1964
2340
  let renderEpoch = 0;
1965
2341
  const stopRender = effect(() => {
1966
2342
  const epoch = ++renderEpoch;
1967
- const { html: rendered, slots: newSlotMap, binds: newBinds } = renderWithCtx(() => fn({
1968
- state,
1969
- derived,
1970
- input
1971
- }), 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
+ });
1972
2360
  const html = stylePrefix + rendered;
1973
2361
  if (!initialized) {
1974
2362
  initialized = true;
@@ -1983,7 +2371,11 @@ var IlhaBuilder = class IlhaBuilder {
1983
2371
  lastRendered = rendered;
1984
2372
  return;
1985
2373
  }
1986
- if (mountedSlots.size > 0 && mountedSlots.size === newSlotMap.size && [...newSlotMap.keys()].every((id) => mountedSlots.has(id)) && isStableInlineSlotMount(initialRenderedHtml, rendered, newSlotMap.keys())) {
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
+ })) {
1987
2379
  pushUpdatedProps(newSlotMap);
1988
2380
  return;
1989
2381
  }
@@ -1994,6 +2386,7 @@ var IlhaBuilder = class IlhaBuilder {
1994
2386
  }
1995
2387
  detachListeners();
1996
2388
  stopBindings();
2389
+ stopJsxEvents();
1997
2390
  const prevMountedCount = mountedSlots.size;
1998
2391
  const leavingPromises = [];
1999
2392
  for (const [id, entry] of mountedSlots) if (!newSlotMap.has(id)) {
@@ -2039,6 +2432,13 @@ var IlhaBuilder = class IlhaBuilder {
2039
2432
  }
2040
2433
  attachListeners();
2041
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
+ });
2042
2442
  mountSlots(newSlotMap);
2043
2443
  };
2044
2444
  if (leavingPromises.length > 0) Promise.allSettled(leavingPromises).then((results) => {
@@ -2051,6 +2451,8 @@ var IlhaBuilder = class IlhaBuilder {
2051
2451
  const unmount = () => {
2052
2452
  if (tornDown) return;
2053
2453
  tornDown = true;
2454
+ disposed = true;
2455
+ unmountController.abort();
2054
2456
  ISLAND_MOUNT_HANDLES.delete(host);
2055
2457
  stopRender();
2056
2458
  detachListeners();
@@ -2098,7 +2500,7 @@ var IlhaBuilder = class IlhaBuilder {
2098
2500
  * The declared return type omits {@link IslandCall} because TypeScript
2099
2501
  * cannot narrow based on runtime stack state.
2100
2502
  */
2101
- const island = function(props) {
2503
+ const island = ((props) => {
2102
2504
  if (currentRenderCtx()) return {
2103
2505
  [ISLAND_CALL]: true,
2104
2506
  island,
@@ -2106,7 +2508,7 @@ var IlhaBuilder = class IlhaBuilder {
2106
2508
  key: void 0
2107
2509
  };
2108
2510
  return renderToString(props);
2109
- };
2511
+ });
2110
2512
  island.toString = (props) => renderToString(props, true);
2111
2513
  island.mount = (host, props) => mountIsland(host, props);
2112
2514
  island[ISLAND_MOUNT_INTERNAL] = (host, props) => mountIslandInternal(host, props);
@@ -2329,6 +2731,7 @@ const rootBuilder = new IlhaBuilder({
2329
2731
  defaultInput: null,
2330
2732
  states: [],
2331
2733
  deriveds: [],
2734
+ actions: [],
2332
2735
  ons: [],
2333
2736
  effects: [],
2334
2737
  onMounts: [],
@@ -2337,7 +2740,17 @@ const rootBuilder = new IlhaBuilder({
2337
2740
  css: null,
2338
2741
  as: null
2339
2742
  });
2340
- 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, {
2341
2754
  html: ilhaHtml,
2342
2755
  raw: ilhaRaw,
2343
2756
  mount: mountAll,
@@ -2349,9 +2762,25 @@ const ilha = Object.assign(rootBuilder, {
2349
2762
  untrack,
2350
2763
  onUncaughtError
2351
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
+ }
2352
2777
  /** @internal Used by the separate JSX runtime entry to preserve island slot composition. */
2353
- function __ilhaJsxSlot(island, props, key) {
2354
- return ilhaRaw(emitIslandSlot(island, props, key));
2778
+ function __ilhaJsxSlot({ island, props, key }) {
2779
+ return ilhaRaw(emitIslandSlot({
2780
+ island,
2781
+ props,
2782
+ key
2783
+ }));
2355
2784
  }
2356
2785
  const html = ilhaHtml;
2357
2786
  const raw = ilhaRaw;
@@ -2361,4 +2790,4 @@ const from = ilhaFrom;
2361
2790
  const context = ilhaContext;
2362
2791
 
2363
2792
  //#endregion
2364
- 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 };