ilha 0.9.5 → 0.10.1

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
@@ -1,7 +1,7 @@
1
1
  import { computed, effect, endBatch, setActiveSub, signal, startBatch } from "alien-signals";
2
2
 
3
3
  //#region src/index.ts
4
- const MAX_SNAPSHOT_CHARS = 256 * 1024;
4
+ const MAX_SNAPSHOT_CHARS = 262144;
5
5
  const MAX_SNAPSHOT_DEPTH = 32;
6
6
  function exceedsMaxDepth(value, depth) {
7
7
  if (depth > MAX_SNAPSHOT_DEPTH) return true;
@@ -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();
@@ -1467,15 +1673,38 @@ var IlhaBuilder = class IlhaBuilder {
1467
1673
  function buildSignalState(input, snapshot) {
1468
1674
  const state = {};
1469
1675
  for (const entry of states) {
1470
- const s = signal(snapshot && entry.key in snapshot ? snapshot[entry.key] : typeof entry.init === "function" ? entry.init(input) : entry.init);
1676
+ const initial = snapshot && entry.key in snapshot ? snapshot[entry.key] : typeof entry.init === "function" ? entry.init(input) : entry.init;
1677
+ const s = signal(initial);
1471
1678
  const accessor = markSignalAccessor((...args) => {
1472
1679
  if (args.length === 0) return s();
1473
- s(args[0]);
1680
+ s(resolveSignalSetter(() => s(), args[0]));
1474
1681
  });
1475
1682
  state[entry.key] = accessor;
1476
1683
  }
1477
1684
  return state;
1478
1685
  }
1686
+ function buildSsrActions() {
1687
+ const out = {};
1688
+ for (const entry of actions) {
1689
+ const invoke = () => {};
1690
+ Object.defineProperties(invoke, {
1691
+ pending: {
1692
+ get: () => false,
1693
+ enumerable: true
1694
+ },
1695
+ data: {
1696
+ get: () => void 0,
1697
+ enumerable: true
1698
+ },
1699
+ error: {
1700
+ get: () => void 0,
1701
+ enumerable: true
1702
+ }
1703
+ });
1704
+ out[entry.key] = invoke;
1705
+ }
1706
+ return out;
1707
+ }
1479
1708
  /** Detached host for SSR setup hooks; must not be `{}` (Areia/components call host.matches). */
1480
1709
  function createSsrOnMountHost() {
1481
1710
  if (typeof document !== "undefined") return document.createElement("div");
@@ -1491,18 +1720,19 @@ var IlhaBuilder = class IlhaBuilder {
1491
1720
  return stub;
1492
1721
  }
1493
1722
  /** Run .onMount() before top-level SSR only (seed module/external state from `input`). */
1494
- function runOnMountForRender(input, state, derived) {
1723
+ function runOnMountForRender({ input, state, derived, action }) {
1495
1724
  if (onMounts.length === 0) return;
1496
1725
  if (currentRenderCtx()) return;
1497
1726
  const host = createSsrOnMountHost();
1498
1727
  const ssrCleanups = [];
1499
1728
  for (const entry of onMounts) {
1500
1729
  const prevSub = setActiveSub(void 0);
1501
- let userCleanup = void 0;
1730
+ let userCleanup;
1502
1731
  try {
1503
1732
  userCleanup = entry.fn({
1504
1733
  state,
1505
1734
  derived,
1735
+ action,
1506
1736
  input,
1507
1737
  host,
1508
1738
  hydrated: false
@@ -1517,6 +1747,7 @@ var IlhaBuilder = class IlhaBuilder {
1517
1747
  source: "mount",
1518
1748
  state,
1519
1749
  derived,
1750
+ action,
1520
1751
  input,
1521
1752
  host
1522
1753
  });
@@ -1540,6 +1771,7 @@ var IlhaBuilder = class IlhaBuilder {
1540
1771
  function renderToString(props, sync = false) {
1541
1772
  const input = resolveInput(props);
1542
1773
  const state = buildSignalState(input);
1774
+ const action = buildSsrActions();
1543
1775
  const results = deriveds.map((entry) => {
1544
1776
  const prevSub = setActiveSub(void 0);
1545
1777
  try {
@@ -1573,14 +1805,20 @@ var IlhaBuilder = class IlhaBuilder {
1573
1805
  error: void 0
1574
1806
  };
1575
1807
  const derived = buildDerivedAccessors(envelopes);
1576
- if (!currentRenderCtx()) runOnMountForRender(input, state, derived);
1808
+ if (!currentRenderCtx()) runOnMountForRender({
1809
+ input,
1810
+ state,
1811
+ derived,
1812
+ action
1813
+ });
1577
1814
  const prevSub = setActiveSub(void 0);
1578
1815
  try {
1579
- const { html } = renderWithCtx(() => fn({
1816
+ const { html } = renderWithCtx({ render: () => fn({
1580
1817
  state,
1581
1818
  derived,
1819
+ action,
1582
1820
  input
1583
- }));
1821
+ }) });
1584
1822
  return stylePrefix + html;
1585
1823
  } finally {
1586
1824
  setActiveSub(prevSub);
@@ -1610,14 +1848,23 @@ var IlhaBuilder = class IlhaBuilder {
1610
1848
  const envelopes = {};
1611
1849
  for (const r of resolved) envelopes[r.key] = r.envelope;
1612
1850
  const derived = buildDerivedAccessors(envelopes);
1613
- if (!currentRenderCtx()) runOnMountForRender(input, state, derived);
1851
+ if (!currentRenderCtx()) runOnMountForRender({
1852
+ input,
1853
+ state,
1854
+ derived,
1855
+ action
1856
+ });
1614
1857
  const prevSub = setActiveSub(void 0);
1615
1858
  try {
1616
- const { html } = await renderWithCtx(() => fn({
1617
- state,
1618
- derived,
1619
- input
1620
- }), void 0, true);
1859
+ const { html } = await renderWithCtx({
1860
+ render: () => fn({
1861
+ state,
1862
+ derived,
1863
+ action,
1864
+ input
1865
+ }),
1866
+ asyncChildren: true
1867
+ });
1621
1868
  return stylePrefix + html;
1622
1869
  } finally {
1623
1870
  setActiveSub(prevSub);
@@ -1684,8 +1931,14 @@ var IlhaBuilder = class IlhaBuilder {
1684
1931
  const cleanups = [];
1685
1932
  const unmountController = new AbortController();
1686
1933
  cleanups.push(() => unmountController.abort());
1687
- const { proxy: derived, setup: setupDerived } = createDerivedProxy(deriveds, state, input, derivedSnapshot);
1934
+ const { proxy: derived, setup: setupDerived } = createDerivedProxy({
1935
+ entries: deriveds,
1936
+ state,
1937
+ input,
1938
+ derivedSnapshot
1939
+ });
1688
1940
  let stopDerived = () => {};
1941
+ let action = {};
1689
1942
  function reportError(err, source) {
1690
1943
  const error = err instanceof Error ? err : new Error(String(err));
1691
1944
  if (onErrors.length === 0) {
@@ -1698,6 +1951,7 @@ var IlhaBuilder = class IlhaBuilder {
1698
1951
  source,
1699
1952
  state,
1700
1953
  derived,
1954
+ action,
1701
1955
  input,
1702
1956
  host
1703
1957
  });
@@ -1705,6 +1959,107 @@ var IlhaBuilder = class IlhaBuilder {
1705
1959
  console.error(handlerErr);
1706
1960
  }
1707
1961
  }
1962
+ const actionEntries = actions;
1963
+ const actionObject = {};
1964
+ let disposed = false;
1965
+ cleanups.push(() => {
1966
+ disposed = true;
1967
+ });
1968
+ for (const entry of actionEntries) {
1969
+ const envelope = signal({
1970
+ pending: 0,
1971
+ data: void 0,
1972
+ error: void 0
1973
+ });
1974
+ const readEnvelope = () => untrack(() => envelope());
1975
+ let latestRun = 0;
1976
+ const invoke = (props) => {
1977
+ if (currentRenderCtx()) return;
1978
+ if (disposed) return;
1979
+ const run = ++latestRun;
1980
+ const runSignal = unmountController.signal;
1981
+ const ctx = {
1982
+ state,
1983
+ derived,
1984
+ input,
1985
+ host,
1986
+ signal: runSignal
1987
+ };
1988
+ let result;
1989
+ let isAsync = false;
1990
+ startBatch();
1991
+ try {
1992
+ result = entry.fn(props, ctx);
1993
+ isAsync = result != null && typeof result.then === "function";
1994
+ const current = readEnvelope();
1995
+ envelope(isAsync ? {
1996
+ pending: current.pending + 1,
1997
+ data: current.data,
1998
+ error: void 0
1999
+ } : {
2000
+ pending: current.pending,
2001
+ data: result,
2002
+ error: void 0
2003
+ });
2004
+ } catch (error) {
2005
+ const normalized = error instanceof Error ? error : new Error(String(error));
2006
+ envelope({
2007
+ pending: readEnvelope().pending,
2008
+ data: void 0,
2009
+ error: normalized
2010
+ });
2011
+ reportError(normalized, "action");
2012
+ return;
2013
+ } finally {
2014
+ endBatch();
2015
+ }
2016
+ if (!isAsync) return;
2017
+ Promise.resolve(result).then((data) => {
2018
+ if (disposed || runSignal.aborted || run !== latestRun) return;
2019
+ const value = readEnvelope();
2020
+ envelope({
2021
+ pending: value.pending,
2022
+ data,
2023
+ error: void 0
2024
+ });
2025
+ }).catch((error) => {
2026
+ if (disposed || runSignal.aborted || error?.name === "AbortError") return;
2027
+ const normalized = error instanceof Error ? error : new Error(String(error));
2028
+ if (run === latestRun) {
2029
+ const value = readEnvelope();
2030
+ envelope({
2031
+ pending: value.pending,
2032
+ data: void 0,
2033
+ error: normalized
2034
+ });
2035
+ }
2036
+ reportError(normalized, "action");
2037
+ }).finally(() => {
2038
+ if (disposed) return;
2039
+ const value = readEnvelope();
2040
+ envelope({
2041
+ ...value,
2042
+ pending: Math.max(0, value.pending - 1)
2043
+ });
2044
+ });
2045
+ };
2046
+ Object.defineProperties(invoke, {
2047
+ pending: {
2048
+ get: () => envelope().pending > 0,
2049
+ enumerable: true
2050
+ },
2051
+ data: {
2052
+ get: () => envelope().data,
2053
+ enumerable: true
2054
+ },
2055
+ error: {
2056
+ get: () => envelope().error,
2057
+ enumerable: true
2058
+ }
2059
+ });
2060
+ actionObject[entry.key] = invoke;
2061
+ }
2062
+ action = actionObject;
1708
2063
  if (transition?.enter) try {
1709
2064
  const result = transition.enter(host);
1710
2065
  if (result instanceof Promise) result.catch((err) => reportError(err, "transition"));
@@ -1736,9 +2091,9 @@ var IlhaBuilder = class IlhaBuilder {
1736
2091
  entry.el.remove();
1737
2092
  }
1738
2093
  function slotBelongsToHost(candidate) {
1739
- let el = candidate;
2094
+ let el = candidate.parentElement;
1740
2095
  while (el && el !== host) {
1741
- if (el.hasAttribute("data-ilha")) return false;
2096
+ if (el.hasAttribute(SLOT_ATTR) || el.hasAttribute("data-ilha")) return false;
1742
2097
  el = el.parentElement;
1743
2098
  }
1744
2099
  return el === host;
@@ -1809,7 +2164,13 @@ var IlhaBuilder = class IlhaBuilder {
1809
2164
  function attachListeners() {
1810
2165
  for (const entry of ons) {
1811
2166
  if (entry.options.once && firedOnce.has(entry)) continue;
1812
- (entry.selector === "" ? [host] : Array.from(host.querySelectorAll(entry.selector))).forEach((listenerTarget) => {
2167
+ let targets;
2168
+ try {
2169
+ targets = entry.selector === "" ? [host] : Array.from(host.querySelectorAll(entry.selector)).filter((element) => templateElementBelongsToHost(host, element));
2170
+ } catch {
2171
+ continue;
2172
+ }
2173
+ targets.forEach((listenerTarget) => {
1813
2174
  const listener = (event) => {
1814
2175
  if (entry.options.once) {
1815
2176
  firedOnce.add(entry);
@@ -1837,6 +2198,7 @@ var IlhaBuilder = class IlhaBuilder {
1837
2198
  result = entry.handler({
1838
2199
  state,
1839
2200
  derived,
2201
+ action,
1840
2202
  input,
1841
2203
  host,
1842
2204
  target: handlerTarget,
@@ -1876,11 +2238,12 @@ var IlhaBuilder = class IlhaBuilder {
1876
2238
  function invokeOnMounts() {
1877
2239
  for (const entry of onMounts) {
1878
2240
  const prevSub = setActiveSub(void 0);
1879
- let userCleanup = void 0;
2241
+ let userCleanup;
1880
2242
  try {
1881
2243
  userCleanup = entry.fn({
1882
2244
  state,
1883
2245
  derived,
2246
+ action,
1884
2247
  input,
1885
2248
  host,
1886
2249
  hydrated
@@ -1904,14 +2267,27 @@ var IlhaBuilder = class IlhaBuilder {
1904
2267
  }
1905
2268
  const preserveSSRDom = hydrated && host.childNodes.length > 0;
1906
2269
  if (hydrated && onMounts.length > 0 && !shouldSkipOnMount) invokeOnMounts();
1907
- const initial = renderWithCtx(() => fn({
1908
- state,
1909
- derived,
1910
- input
1911
- }), preserveSSRDom ? host : void 0);
2270
+ const nativeOnceFired = /* @__PURE__ */ new WeakMap();
2271
+ const initial = renderWithCtx({
2272
+ render: () => fn({
2273
+ state,
2274
+ derived,
2275
+ action,
2276
+ input
2277
+ }),
2278
+ liveHost: preserveSSRDom ? host : void 0
2279
+ });
1912
2280
  if (!preserveSSRDom) host.innerHTML = stylePrefix + initial.html;
1913
2281
  let stopBindings = applyTemplateBindings(host, initial.binds);
1914
2282
  cleanups.push(() => stopBindings());
2283
+ let stopJsxEvents = applyJsxEvents({
2284
+ host,
2285
+ records: initial.events,
2286
+ reportError: (error) => reportError(error, "on"),
2287
+ unmountSignal: unmountController.signal,
2288
+ onceFired: nativeOnceFired
2289
+ });
2290
+ cleanups.push(() => stopJsxEvents());
1915
2291
  mountSlots(initial.slots);
1916
2292
  cleanups.push(() => mountedSlots.forEach((entry) => entry.unmount()));
1917
2293
  attachListeners();
@@ -1936,6 +2312,7 @@ var IlhaBuilder = class IlhaBuilder {
1936
2312
  userCleanup = entry.fn({
1937
2313
  state,
1938
2314
  derived,
2315
+ action,
1939
2316
  input,
1940
2317
  host,
1941
2318
  signal: runSignal
@@ -1964,11 +2341,23 @@ var IlhaBuilder = class IlhaBuilder {
1964
2341
  let renderEpoch = 0;
1965
2342
  const stopRender = effect(() => {
1966
2343
  const epoch = ++renderEpoch;
1967
- const { html: rendered, slots: newSlotMap, binds: newBinds } = renderWithCtx(() => fn({
1968
- state,
1969
- derived,
1970
- input
1971
- }), host);
2344
+ const { html: rendered, slots: newSlotMap, binds: newBinds, events: newEvents } = renderWithCtx({
2345
+ render: () => fn({
2346
+ state,
2347
+ derived,
2348
+ action,
2349
+ input
2350
+ }),
2351
+ liveHost: host
2352
+ });
2353
+ stopJsxEvents();
2354
+ stopJsxEvents = applyJsxEvents({
2355
+ host,
2356
+ records: newEvents,
2357
+ reportError: (error) => reportError(error, "on"),
2358
+ unmountSignal: unmountController.signal,
2359
+ onceFired: nativeOnceFired
2360
+ });
1972
2361
  const html = stylePrefix + rendered;
1973
2362
  if (!initialized) {
1974
2363
  initialized = true;
@@ -1983,7 +2372,11 @@ var IlhaBuilder = class IlhaBuilder {
1983
2372
  lastRendered = rendered;
1984
2373
  return;
1985
2374
  }
1986
- if (mountedSlots.size > 0 && mountedSlots.size === newSlotMap.size && [...newSlotMap.keys()].every((id) => mountedSlots.has(id)) && isStableInlineSlotMount(initialRenderedHtml, rendered, newSlotMap.keys())) {
2375
+ if (mountedSlots.size > 0 && mountedSlots.size === newSlotMap.size && [...newSlotMap.keys()].every((id) => mountedSlots.has(id)) && isStableInlineSlotMount({
2376
+ initialHtml: initialRenderedHtml,
2377
+ renderedHtml: rendered,
2378
+ slotIds: newSlotMap.keys()
2379
+ })) {
1987
2380
  pushUpdatedProps(newSlotMap);
1988
2381
  return;
1989
2382
  }
@@ -1994,6 +2387,7 @@ var IlhaBuilder = class IlhaBuilder {
1994
2387
  }
1995
2388
  detachListeners();
1996
2389
  stopBindings();
2390
+ stopJsxEvents();
1997
2391
  const prevMountedCount = mountedSlots.size;
1998
2392
  const leavingPromises = [];
1999
2393
  for (const [id, entry] of mountedSlots) if (!newSlotMap.has(id)) {
@@ -2039,6 +2433,13 @@ var IlhaBuilder = class IlhaBuilder {
2039
2433
  }
2040
2434
  attachListeners();
2041
2435
  stopBindings = applyTemplateBindings(host, newBinds);
2436
+ stopJsxEvents = applyJsxEvents({
2437
+ host,
2438
+ records: newEvents,
2439
+ reportError: (error) => reportError(error, "on"),
2440
+ unmountSignal: unmountController.signal,
2441
+ onceFired: nativeOnceFired
2442
+ });
2042
2443
  mountSlots(newSlotMap);
2043
2444
  };
2044
2445
  if (leavingPromises.length > 0) Promise.allSettled(leavingPromises).then((results) => {
@@ -2051,6 +2452,8 @@ var IlhaBuilder = class IlhaBuilder {
2051
2452
  const unmount = () => {
2052
2453
  if (tornDown) return;
2053
2454
  tornDown = true;
2455
+ disposed = true;
2456
+ unmountController.abort();
2054
2457
  ISLAND_MOUNT_HANDLES.delete(host);
2055
2458
  stopRender();
2056
2459
  detachListeners();
@@ -2098,7 +2501,7 @@ var IlhaBuilder = class IlhaBuilder {
2098
2501
  * The declared return type omits {@link IslandCall} because TypeScript
2099
2502
  * cannot narrow based on runtime stack state.
2100
2503
  */
2101
- const island = function(props) {
2504
+ const island = ((props) => {
2102
2505
  if (currentRenderCtx()) return {
2103
2506
  [ISLAND_CALL]: true,
2104
2507
  island,
@@ -2106,7 +2509,7 @@ var IlhaBuilder = class IlhaBuilder {
2106
2509
  key: void 0
2107
2510
  };
2108
2511
  return renderToString(props);
2109
- };
2512
+ });
2110
2513
  island.toString = (props) => renderToString(props, true);
2111
2514
  island.mount = (host, props) => mountIsland(host, props);
2112
2515
  island[ISLAND_MOUNT_INTERNAL] = (host, props) => mountIslandInternal(host, props);
@@ -2329,6 +2732,7 @@ const rootBuilder = new IlhaBuilder({
2329
2732
  defaultInput: null,
2330
2733
  states: [],
2331
2734
  deriveds: [],
2735
+ actions: [],
2332
2736
  ons: [],
2333
2737
  effects: [],
2334
2738
  onMounts: [],
@@ -2337,7 +2741,17 @@ const rootBuilder = new IlhaBuilder({
2337
2741
  css: null,
2338
2742
  as: null
2339
2743
  });
2340
- const ilha = Object.assign(rootBuilder, {
2744
+ const callableRoot = ((fn) => rootBuilder.input().render(fn));
2745
+ Object.defineProperty(callableRoot, "_cfg", {
2746
+ value: rootBuilder._cfg,
2747
+ enumerable: false
2748
+ });
2749
+ for (const key of Object.getOwnPropertyNames(IlhaBuilder.prototype)) {
2750
+ if (key === "constructor") continue;
2751
+ const descriptor = Object.getOwnPropertyDescriptor(IlhaBuilder.prototype, key);
2752
+ if (descriptor) Object.defineProperty(callableRoot, key, descriptor);
2753
+ }
2754
+ const ilha = Object.assign(callableRoot, {
2341
2755
  html: ilhaHtml,
2342
2756
  raw: ilhaRaw,
2343
2757
  mount: mountAll,
@@ -2349,9 +2763,25 @@ const ilha = Object.assign(rootBuilder, {
2349
2763
  untrack,
2350
2764
  onUncaughtError
2351
2765
  });
2766
+ /** @internal Used by the separate JSX runtime to register a native event handler. */
2767
+ function __ilhaJsxEvent({ type, handler, modifier }) {
2768
+ const ctx = currentRenderCtx();
2769
+ if (!ctx) return void 0;
2770
+ const index = ctx.events.length;
2771
+ ctx.events.push({
2772
+ type,
2773
+ handler,
2774
+ modifier
2775
+ });
2776
+ return index;
2777
+ }
2352
2778
  /** @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));
2779
+ function __ilhaJsxSlot({ island, props, key }) {
2780
+ return ilhaRaw(emitIslandSlot({
2781
+ island,
2782
+ props,
2783
+ key
2784
+ }));
2355
2785
  }
2356
2786
  const html = ilhaHtml;
2357
2787
  const raw = ilhaRaw;
@@ -2361,4 +2791,4 @@ const from = ilhaFrom;
2361
2791
  const context = ilhaContext;
2362
2792
 
2363
2793
  //#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 };
2794
+ 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 };