ilha 0.14.7 → 0.14.9

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/define.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as mount } from "./mount-rXNnaIWf.js";
1
+ import { t as mount } from "./mount-BzNrfVW4.js";
2
2
 
3
3
  //#region src/define.ts
4
4
  const define = (name, component) => {
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as makeFiber, a as interpret, c as handleOwner, d as structuralEqual, f as untrack, g as getFiber, h as closeFiber, i as runSetup, l as instr, m as clearFiberView, n as renderToString, o as atom, p as wrapHandle, r as unsafe, s as batch, t as mount, u as isAtomHandle } from "./mount-rXNnaIWf.js";
1
+ import { _ as makeFiber, a as interpret, c as handleOwner, d as structuralEqual, f as untrack, g as getFiber, h as closeFiber, i as runSetup, l as instr, m as clearFiberView, n as renderToString, o as atom, p as wrapHandle, r as unsafe, s as batch, t as mount, u as isAtomHandle } from "./mount-BzNrfVW4.js";
2
2
  import { d as errorView, g as isString, i as Fragment, p as isFunction, t as h } from "./vnode-M-oJMKcv.js";
3
3
  import * as Atom from "effect/reactivity/Atom";
4
4
  import * as Effect from "effect/Effect";
@@ -241,6 +241,18 @@ const watch = Object.assign(watchSource, { once: watchOnce });
241
241
  const sharedCache = /* @__PURE__ */ new Map();
242
242
  const liveRefetchers = /* @__PURE__ */ new Map();
243
243
  const cells$1 = /* @__PURE__ */ new WeakMap();
244
+ /** Unmount: stop every client cell of one fiber (all keys it ever used). */
245
+ const releaseCells = (byKey) => {
246
+ for (const [key, entry] of byKey) {
247
+ const c = entry;
248
+ c.alive = false;
249
+ c.ctrl?.abort();
250
+ const live = liveRefetchers.get(key);
251
+ live?.delete(c.onInvalidate);
252
+ if (live && live.size === 0) liveRefetchers.delete(key);
253
+ }
254
+ byKey.clear();
255
+ };
244
256
  const entryFor = (key) => sharedCache.get(key);
245
257
  /** Drop the cached value for `key` and refetch it in live components. */
246
258
  const invalidate = (key) => {
@@ -325,10 +337,13 @@ const resource = (key, fetcher, opts) => {
325
337
  }
326
338
  let cell = byKey.get(key);
327
339
  if (!cell) {
328
- cell = {
340
+ const fresh = {
329
341
  alive: true,
330
342
  ctrl: null,
331
- held: false,
343
+ onInvalidate: () => {
344
+ fresh.revalidate();
345
+ },
346
+ revalidate: () => {},
332
347
  sink: {
333
348
  data,
334
349
  error,
@@ -336,7 +351,14 @@ const resource = (key, fetcher, opts) => {
336
351
  },
337
352
  started: false
338
353
  };
354
+ cell = fresh;
339
355
  byKey.set(key, cell);
356
+ let live = liveRefetchers.get(key);
357
+ if (!live) {
358
+ live = /* @__PURE__ */ new Set();
359
+ liveRefetchers.set(key, live);
360
+ }
361
+ live.add(cell.onInvalidate);
340
362
  }
341
363
  cell.sink = {
342
364
  data,
@@ -411,6 +433,9 @@ const resource = (key, fetcher, opts) => {
411
433
  await start();
412
434
  } catch {}
413
435
  };
436
+ c.revalidate = () => {
437
+ settleStart();
438
+ };
414
439
  if (!c.started) {
415
440
  c.started = true;
416
441
  const swr = opts?.staleWhileRevalidate ?? true;
@@ -439,28 +464,9 @@ const resource = (key, fetcher, opts) => {
439
464
  }
440
465
  }
441
466
  }
442
- if (!c.held) {
443
- c.held = true;
444
- watch.once(() => {
445
- let live = liveRefetchers.get(key);
446
- if (!live) {
447
- live = /* @__PURE__ */ new Set();
448
- liveRefetchers.set(key, live);
449
- }
450
- const onInvalidate = () => {
451
- settleStart();
452
- };
453
- live.add(onInvalidate);
454
- return () => {
455
- c.alive = false;
456
- c.ctrl?.abort();
457
- const s = liveRefetchers.get(key);
458
- s?.delete(onInvalidate);
459
- if (s && s.size === 0) liveRefetchers.delete(key);
460
- if (fiberCells.get(key) === c) fiberCells.delete(key);
461
- };
462
- });
463
- }
467
+ watch.once(() => () => {
468
+ releaseCells(fiberCells);
469
+ });
464
470
  return {
465
471
  data,
466
472
  error,
@@ -488,22 +494,22 @@ const connect = ({ cell: c, decode, event, retryBase, url }) => {
488
494
  for (const sink of c.sinks.values()) sink.latest.set(value);
489
495
  for (const push of c.targets) push(value);
490
496
  };
497
+ const onMessage = (ev) => {
498
+ const raw = ev.data;
499
+ if (!isString(raw)) return;
500
+ let value;
501
+ try {
502
+ value = decode(raw);
503
+ } catch {
504
+ return;
505
+ }
506
+ emit(value);
507
+ };
491
508
  const open = () => {
492
509
  if (disposed) return;
493
510
  setStatus(c, attempt === 0 ? "connecting" : "retrying");
494
511
  const es = new EventSource(url);
495
512
  source = es;
496
- const onMessage = (ev) => {
497
- const raw = ev.data;
498
- if (!isString(raw)) return;
499
- let value;
500
- try {
501
- value = decode(raw);
502
- } catch {
503
- return;
504
- }
505
- emit(value);
506
- };
507
513
  const onOpen = () => {
508
514
  attempt = 0;
509
515
  setStatus(c, "open");
@@ -512,39 +512,71 @@ const eventTypeFromProp = (key) => {
512
512
  return type.length >= 3 ? type : void 0;
513
513
  };
514
514
  const isEventProp = (key) => eventTypeFromProp(key) !== void 0;
515
+ const dispatch = (el, type, ev) => {
516
+ const fiber = el.__ilhaFiber;
517
+ if (!fiber || fiber.closed) return;
518
+ const fn = el.__ilhaHandlers?.get(type);
519
+ if (!isFunction(fn)) return;
520
+ const out = fn(ev);
521
+ if (Effect$1.isEffect(out)) fiber.run(out);
522
+ };
523
+ /** One native listener per type; it reads the latest handler at dispatch. */
524
+ const listen = (el, type) => {
525
+ el.__ilhaEvents ??= /* @__PURE__ */ new Set();
526
+ if (el.__ilhaEvents.has(type)) return;
527
+ el.__ilhaEvents.add(type);
528
+ el.addEventListener(type, (ev) => {
529
+ dispatch(el, type, ev);
530
+ });
531
+ };
532
+ const bindSsrActions = (el, props, fiber) => {
533
+ const seen = /* @__PURE__ */ new Set();
534
+ for (const key of Object.keys(props)) {
535
+ const type = eventTypeFromProp(key);
536
+ if (!type || seen.has(type)) continue;
537
+ seen.add(type);
538
+ const fn = props[key];
539
+ if (!isFunction(fn)) continue;
540
+ const branded = fn[ACTION_CALL$1];
541
+ const rec = branded?.k ? {
542
+ a: branded.a ?? [],
543
+ k: branded.k
544
+ } : void 0;
545
+ if (!rec) continue;
546
+ const id = `${type}:${fiber.runtime.ssrEventI}`;
547
+ fiber.runtime.ssrEventI += 1;
548
+ fiber.runtime.ssrActions[id] = rec;
549
+ const existing = el.dataset.ilhaOn;
550
+ el.dataset.ilhaOn = existing ? `${existing},${id}` : id;
551
+ }
552
+ };
515
553
  const bindEvents = (node, props, fiber) => {
516
554
  const el = node;
517
- el.__ilhaProps = props;
518
- el.__ilhaEvents ??= /* @__PURE__ */ new Set();
555
+ if (fiber.runtime.ssr) {
556
+ bindSsrActions(el, props, fiber);
557
+ return;
558
+ }
559
+ const handlers = /* @__PURE__ */ new Map();
519
560
  for (const key of Object.keys(props)) {
520
561
  const type = eventTypeFromProp(key);
521
- if (!type) continue;
522
- if (el.__ilhaEvents.has(type)) continue;
523
- el.__ilhaEvents.add(type);
524
- if (fiber.runtime.ssr) {
525
- const fn = props[key];
526
- if (!isFunction(fn)) continue;
527
- const branded = fn[ACTION_CALL$1];
528
- const rec = branded?.k ? {
529
- a: branded.a ?? [],
530
- k: branded.k
531
- } : void 0;
532
- if (!rec) continue;
533
- const id = `${type}:${fiber.runtime.ssrEventI}`;
534
- fiber.runtime.ssrEventI += 1;
535
- fiber.runtime.ssrActions[id] = rec;
536
- const existing = el.dataset.ilhaOn;
537
- el.dataset.ilhaOn = existing ? `${existing},${id}` : id;
538
- continue;
539
- }
540
- el.addEventListener(type, (ev) => {
541
- if (fiber.closed) return;
542
- const fn = el.__ilhaProps?.[key];
543
- if (!isFunction(fn)) return;
544
- const out = fn(ev);
545
- if (Effect$1.isEffect(out)) fiber.run(out);
546
- });
562
+ if (type && !handlers.has(type)) handlers.set(type, props[key]);
547
563
  }
564
+ el.__ilhaHandlers = handlers;
565
+ el.__ilhaFiber = fiber;
566
+ for (const type of handlers.keys()) listen(el, type);
567
+ };
568
+ /**
569
+ * The morph kept `live` in place of the freshly rendered `fresh`: hand the
570
+ * render's handlers to the live node, so closures never go stale and handlers
571
+ * added on a later render still fire.
572
+ */
573
+ const adoptEvents = (live, fresh) => {
574
+ const from = fresh;
575
+ if (!from.__ilhaFiber) return;
576
+ const to = live;
577
+ to.__ilhaHandlers = from.__ilhaHandlers;
578
+ to.__ilhaFiber = from.__ilhaFiber;
579
+ for (const type of from.__ilhaHandlers?.keys() ?? []) listen(to, type);
548
580
  };
549
581
 
550
582
  //#endregion
@@ -556,14 +588,17 @@ const morphKeyOf = (el) => {
556
588
  const s = el.getAttribute(SLOT_ATTR);
557
589
  return s === null ? null : `s:${s}`;
558
590
  };
591
+ /** Whether `data-morph-preserve` on `el` lists `token`. */
592
+ const hasPreserveToken = (el, token) => {
593
+ const custom = el.getAttribute(PRESERVE_ATTR);
594
+ if (custom === null) return false;
595
+ for (const listed of custom.split(/\s+/u)) if (listed === token) return true;
596
+ return false;
597
+ };
559
598
  const shouldPreserveMorphAttr = (el, name) => {
560
599
  if (name === "value" || name === "checked" || name === "selected") return true;
561
600
  if (name === PRESERVE_ATTR) return el.hasAttribute(PRESERVE_ATTR);
562
- const custom = el.getAttribute(PRESERVE_ATTR);
563
- if (custom !== null) {
564
- for (const token of custom.split(/\s+/u)) if (token === name) return true;
565
- }
566
- return false;
601
+ return hasPreserveToken(el, name);
567
602
  };
568
603
  const syncAttributes = (from, to) => {
569
604
  for (const { name, value } of to.attributes) {
@@ -578,9 +613,26 @@ const syncAttributes = (from, to) => {
578
613
  if (!to.hasAttribute(name)) from.removeAttribute(name);
579
614
  }
580
615
  };
616
+ /** Deepest focused element, looking through open shadow roots. */
617
+ const deepActiveElement = () => {
618
+ let active = document.activeElement;
619
+ while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
620
+ return active;
621
+ };
622
+ const snapshotRanges = (active) => {
623
+ if (!active.isContentEditable) return null;
624
+ const sel = document.getSelection();
625
+ if (!sel || sel.rangeCount === 0) return null;
626
+ const ranges = [];
627
+ for (let i = 0; i < sel.rangeCount; i += 1) {
628
+ const range = sel.getRangeAt(i);
629
+ if (active.contains(range.startContainer)) ranges.push(range.cloneRange());
630
+ }
631
+ return ranges.length > 0 ? ranges : null;
632
+ };
581
633
  const snapshotFocus = () => {
582
634
  if (globalThis.document === void 0) return null;
583
- const active = document.activeElement;
635
+ const active = deepActiveElement();
584
636
  if (!(active instanceof HTMLElement) || active === document.body) return null;
585
637
  let selection = null;
586
638
  try {
@@ -592,13 +644,23 @@ const snapshotFocus = () => {
592
644
  } catch {}
593
645
  return {
594
646
  active,
647
+ ranges: snapshotRanges(active),
595
648
  selection
596
649
  };
597
650
  };
651
+ const restoreRanges = (ranges) => {
652
+ const sel = document.getSelection();
653
+ if (!sel || !ranges.every((r) => r.startContainer.isConnected)) return;
654
+ sel.removeAllRanges();
655
+ for (const r of ranges) sel.addRange(r);
656
+ };
598
657
  const restoreFocus = (snapshot) => {
599
658
  if (!snapshot || !snapshot.active.isConnected) return;
600
659
  try {
601
- if (document.activeElement !== snapshot.active) snapshot.active.focus({ preventScroll: true });
660
+ if (deepActiveElement() !== snapshot.active) {
661
+ snapshot.active.focus({ preventScroll: true });
662
+ if (snapshot.ranges) restoreRanges(snapshot.ranges);
663
+ }
602
664
  const sel = snapshot.selection;
603
665
  if (sel && sel.start !== null) {
604
666
  const el = snapshot.active;
@@ -606,6 +668,122 @@ const restoreFocus = (snapshot) => {
606
668
  }
607
669
  } catch {}
608
670
  };
671
+ const STAND_IN_ATTR = "data-ilha-reuse";
672
+ const standIns = /* @__PURE__ */ new WeakMap();
673
+ /** Live nodes with an outstanding stand-in: the morph must not consume them. */
674
+ const claimed = /* @__PURE__ */ new WeakSet();
675
+ /** Placeholder for a connected live node inside a scratch render tree. */
676
+ const standInFor = (live) => {
677
+ const el = document.createElementNS(live.namespaceURI, live.localName);
678
+ for (const name of [SLOT_ATTR, KEY_ATTR]) {
679
+ const v = live.getAttribute(name);
680
+ if (v !== null) el.setAttribute(name, v);
681
+ }
682
+ el.setAttribute(STAND_IN_ATTR, "");
683
+ standIns.set(el, live);
684
+ claimed.add(live);
685
+ return el;
686
+ };
687
+ const liveOf = (node) => {
688
+ const live = standIns.get(node);
689
+ if (!live) return node;
690
+ claimed.delete(live);
691
+ return live;
692
+ };
693
+ /**
694
+ * Insert `node` before `ref`. Connected nodes move with `moveBefore` where the
695
+ * browser has it, so they keep focus, iframe, dialog, and element state.
696
+ */
697
+ const place = (parent, node, ref) => {
698
+ if (node === ref || node.parentNode === parent && node.nextSibling === ref) return;
699
+ if (node.isConnected && parent.isConnected && parent.moveBefore) try {
700
+ parent.moveBefore(node, ref);
701
+ return;
702
+ } catch {}
703
+ if (ref) ref.before(node);
704
+ else parent.append(node);
705
+ };
706
+ /** Swap stand-ins nested in `node` (now in the document) for live nodes. */
707
+ const resolveNested = (node) => {
708
+ if (!(node instanceof Element) || !node.firstElementChild) return;
709
+ const nested = node.querySelectorAll(`[${STAND_IN_ATTR}]`);
710
+ for (const s of nested) {
711
+ const live = liveOf(s);
712
+ if (live !== s && s.parentNode) {
713
+ place(s.parentNode, live, s);
714
+ s.remove();
715
+ }
716
+ }
717
+ };
718
+ /**
719
+ * Put a rendered node into the live tree: stand-ins become their live nodes,
720
+ * and stand-ins nested in fresh subtrees resolve once the subtree is attached.
721
+ */
722
+ const insertRendered = (parent, node, ref) => {
723
+ const live = liveOf(node);
724
+ place(parent, live, ref);
725
+ if (live === node) resolveNested(node);
726
+ };
727
+ /** Longest increasing run of old indexes: those nodes never move. */
728
+ const stableSet = (nodes, oldIndex) => {
729
+ const tails = [];
730
+ const prev = [];
731
+ for (let i = 0; i < nodes.length; i += 1) {
732
+ const node = nodes[i];
733
+ const idx = node ? oldIndex.get(node) : void 0;
734
+ if (idx === void 0) {
735
+ prev.push(-1);
736
+ continue;
737
+ }
738
+ let lo = 0;
739
+ let hi = tails.length;
740
+ while (lo < hi) {
741
+ const mid = Math.floor((lo + hi) / 2);
742
+ const tail = nodes[tails[mid] ?? 0];
743
+ if ((tail ? oldIndex.get(tail) ?? 0 : 0) < idx) lo = mid + 1;
744
+ else hi = mid;
745
+ }
746
+ prev.push(lo > 0 ? tails[lo - 1] ?? -1 : -1);
747
+ tails[lo] = i;
748
+ }
749
+ const stable = /* @__PURE__ */ new Set();
750
+ let k = tails.length > 0 ? tails.at(-1) ?? -1 : -1;
751
+ while (k >= 0) {
752
+ const node = nodes[k];
753
+ if (node) stable.add(node);
754
+ k = prev[k] ?? -1;
755
+ }
756
+ return stable;
757
+ };
758
+ /**
759
+ * Make `parent`'s children exactly `nodes` with the fewest moves. Nodes that
760
+ * keep their relative order stay attached; removed nodes leave first.
761
+ */
762
+ const placeChildren = (parent, rendered) => {
763
+ const focus = snapshotFocus();
764
+ const nodes = rendered.map(liveOf);
765
+ const wanted = new Set(nodes);
766
+ const oldIndex = /* @__PURE__ */ new Map();
767
+ let child = parent.firstChild;
768
+ while (child) {
769
+ const next = child.nextSibling;
770
+ if (wanted.has(child)) oldIndex.set(child, oldIndex.size);
771
+ else child.remove();
772
+ child = next;
773
+ }
774
+ const stable = stableSet(nodes, oldIndex);
775
+ let next = null;
776
+ for (let i = nodes.length - 1; i >= 0; i -= 1) {
777
+ const node = nodes[i];
778
+ if (!node) continue;
779
+ if (!stable.has(node)) {
780
+ place(parent, node, next);
781
+ if (!oldIndex.has(node)) resolveNested(node);
782
+ }
783
+ next = node;
784
+ }
785
+ restoreFocus(focus);
786
+ };
609
787
  const buildKeyedIndex = (parent) => {
610
788
  let fromKeyed = null;
611
789
  for (const child of parent.children) {
@@ -667,18 +845,43 @@ const morphSelect = (fromEl, toEl, api) => {
667
845
  if (prev && o.selected !== prev.live) o.selected = prev.live;
668
846
  }
669
847
  };
848
+ /** Rendered element → the live element the morph kept in its place. */
849
+ const adoptedBy = /* @__PURE__ */ new WeakMap();
850
+ /** The element a rendered element ended up as in the document. */
851
+ const committedElement = (el) => adoptedBy.get(el) ?? el;
852
+ /** `live` stays in the document for `fresh`: move the render's bindings. */
853
+ const adopt = (live, fresh) => {
854
+ adoptedBy.set(fresh, live);
855
+ adoptEvents(live, fresh);
856
+ };
857
+ /**
858
+ * A contenteditable the render leaves empty is uncontrolled: the user's
859
+ * content (and caret) survives, like an uncontrolled textarea.
860
+ */
861
+ const isUncontrolledEditable = (from, to) => {
862
+ const mode = to.getAttribute("contenteditable");
863
+ return mode !== null && mode !== "false" && !to.hasChildNodes() && from.getAttribute("contenteditable") === mode;
864
+ };
865
+ /** Swap `fromNode` for a rendered node without ever detaching live nodes. */
866
+ const replaceRendered = (fromNode, toNode) => {
867
+ const parent = fromNode.parentNode;
868
+ if (!parent) return;
869
+ insertRendered(parent, toNode, fromNode);
870
+ fromNode.remove();
871
+ };
670
872
  const morphElementPair = (fromEl, toEl, api) => {
671
873
  if (fromEl.localName !== toEl.localName) {
672
- fromEl.replaceWith(toEl);
874
+ replaceRendered(fromEl, toEl);
673
875
  return;
674
876
  }
675
877
  const toSlot = toEl.getAttribute(SLOT_ATTR);
676
878
  const fromSlot = fromEl.getAttribute(SLOT_ATTR);
677
879
  if (toSlot !== null && fromSlot === toSlot) return;
678
880
  if (toSlot !== null) {
679
- fromEl.replaceWith(toEl);
881
+ replaceRendered(fromEl, toEl);
680
882
  return;
681
883
  }
884
+ adopt(fromEl, toEl);
682
885
  if (fromEl.localName === "input") {
683
886
  morphInput(fromEl, toEl);
684
887
  return;
@@ -687,10 +890,18 @@ const morphElementPair = (fromEl, toEl, api) => {
687
890
  morphSelect(fromEl, toEl, api);
688
891
  return;
689
892
  }
893
+ const keepChildren = hasPreserveToken(fromEl, "children") || hasPreserveToken(toEl, "children") || isUncontrolledEditable(fromEl, toEl);
690
894
  syncAttributes(fromEl, toEl);
895
+ if (keepChildren) return;
691
896
  if (fromEl.localName === "textarea") morphTextarea(fromEl, toEl);
692
897
  else api.morphChildren(fromEl, toEl);
693
898
  };
899
+ /** Keyed node the new render drops and no stand-in still needs. */
900
+ const isStaleKeyed = (node, toKeys) => {
901
+ if (!(node instanceof Element) || claimed.has(node)) return false;
902
+ const key = morphKeyOf(node);
903
+ return key !== null && !toKeys.has(key);
904
+ };
694
905
  const alignKeyedNode = (fromParent, fromNode, toNode, fromKeyed, toKeys) => {
695
906
  const toKey = toNode.nodeType === 1 ? morphKeyOf(toNode) : null;
696
907
  let current = fromNode;
@@ -698,9 +909,13 @@ const alignKeyedNode = (fromParent, fromNode, toNode, fromKeyed, toKeys) => {
698
909
  const match = fromKeyed.get(toKey);
699
910
  if (match) {
700
911
  fromKeyed.delete(toKey);
912
+ while (current && current !== match && isStaleKeyed(current, toKeys)) {
913
+ const next = current.nextSibling;
914
+ current.remove();
915
+ current = next ?? void 0;
916
+ }
701
917
  if (match !== current) {
702
- current?.before(match);
703
- if (!current) fromParent.append(match);
918
+ place(fromParent, match, current ?? null);
704
919
  current = match;
705
920
  }
706
921
  }
@@ -708,12 +923,31 @@ const alignKeyedNode = (fromParent, fromNode, toNode, fromKeyed, toKeys) => {
708
923
  if (current instanceof Element) {
709
924
  const fromKey = morphKeyOf(current);
710
925
  if (fromKey !== null && fromKey !== toKey && toKeys.has(fromKey)) {
711
- current.before(toNode);
926
+ insertRendered(fromParent, toNode, current);
712
927
  return "continue";
713
928
  }
714
929
  }
715
930
  return current;
716
931
  };
932
+ const morphChild = ({ api, fromParent, fromNode, toNode }) => {
933
+ if (standIns.has(toNode)) {
934
+ insertRendered(fromParent, toNode, fromNode ?? null);
935
+ return;
936
+ }
937
+ if (!fromNode || claimed.has(fromNode)) {
938
+ insertRendered(fromParent, toNode, fromNode ?? null);
939
+ return;
940
+ }
941
+ if (fromNode.nodeType !== toNode.nodeType) {
942
+ replaceRendered(fromNode, toNode);
943
+ return;
944
+ }
945
+ if (fromNode.nodeType === 3 || fromNode.nodeType === 8) {
946
+ if (fromNode.nodeValue !== toNode.nodeValue) fromNode.nodeValue = toNode.nodeValue;
947
+ return;
948
+ }
949
+ if (fromNode.nodeType === 1) morphElementPair(fromNode, toNode, api);
950
+ };
717
951
  const api = { morphChildren: (fromParent, toParent) => {
718
952
  const toNodes = [...toParent.childNodes];
719
953
  const fromKeyed = buildKeyedIndex(fromParent);
@@ -727,19 +961,12 @@ const api = { morphChildren: (fromParent, toParent) => {
727
961
  if (aligned === "continue") continue;
728
962
  fromNode = aligned;
729
963
  }
730
- if (!fromNode) {
731
- fromParent.append(toNode);
732
- continue;
733
- }
734
- if (fromNode.nodeType !== toNode.nodeType) {
735
- fromNode.replaceWith(toNode);
736
- continue;
737
- }
738
- if (fromNode.nodeType === 3 || fromNode.nodeType === 8) {
739
- if (fromNode.nodeValue !== toNode.nodeValue) fromNode.nodeValue = toNode.nodeValue;
740
- continue;
741
- }
742
- if (fromNode.nodeType === 1) morphElementPair(fromNode, toNode, api);
964
+ morphChild({
965
+ api,
966
+ fromNode,
967
+ fromParent,
968
+ toNode
969
+ });
743
970
  }
744
971
  while (fromParent.childNodes.length > toNodes.length) fromParent.lastChild?.remove();
745
972
  } };
@@ -897,6 +1124,21 @@ const sweepComponentSlots = (fiber) => {
897
1124
  }
898
1125
  slots.length = cframe.i;
899
1126
  };
1127
+ const sweepIslandSlots = (fiber) => {
1128
+ const frame = fiber.islandFrame;
1129
+ if (!frame) return;
1130
+ const active = new Set(frame.slots.slice(0, frame.i));
1131
+ const keep = [];
1132
+ for (const h of fiber.holes) {
1133
+ if (h.islandSlot && !active.has(h.islandSlot)) {
1134
+ h.dispose();
1135
+ continue;
1136
+ }
1137
+ keep.push(h);
1138
+ }
1139
+ fiber.holes = keep;
1140
+ frame.slots.length = Math.min(frame.slots.length, frame.i);
1141
+ };
900
1142
  const findReusableAtomHost = (fiber, atom) => {
901
1143
  for (const h of fiber.holes) if (h.atom === atom && h.host?.isConnected && h.holeFiber && !h.holeFiber.closed) return {
902
1144
  hole: h.holeFiber,
@@ -933,14 +1175,28 @@ const isSkippedPropKey = (k) => k === "children" || k === "key" || k === "ref" |
933
1175
  * keyed reconciliation. Host differences live in `PaintOps`.
934
1176
  */
935
1177
  const createPainter = (ops) => {
1178
+ let paintDepth = 0;
1179
+ const pendingRefs = [];
1180
+ const commit = (fn) => {
1181
+ paintDepth += 1;
1182
+ try {
1183
+ fn();
1184
+ } finally {
1185
+ paintDepth -= 1;
1186
+ if (paintDepth === 0) for (const run of pendingRefs.splice(0)) run();
1187
+ }
1188
+ };
936
1189
  const applyFormControl = (el, key, v, fiber) => {
937
- const setProp = (x) => ops.setFormControl(el, key, x);
1190
+ const setProp = (x) => ops.setFormControl(ops.committed(el), key, x);
938
1191
  if (isAtomHandle(v)) {
939
1192
  const memo = el;
940
1193
  if (memo.__ilhaVal === v) return;
941
1194
  memo.__ilhaVal = v;
942
1195
  const unsub = fiber.registry.subscribe(v.atom, setProp, { immediate: true });
943
- fiber.holes.push({ dispose: unsub });
1196
+ fiber.holes.push({ dispose: () => {
1197
+ unsub();
1198
+ if (memo.__ilhaVal === v) memo.__ilhaVal = void 0;
1199
+ } });
944
1200
  } else setProp(v);
945
1201
  };
946
1202
  const trackHole = (parent, hole, extra, meta) => {
@@ -986,7 +1242,9 @@ const createPainter = (ops) => {
986
1242
  const { ref } = props;
987
1243
  if (isFunction(ref)) {
988
1244
  const refFn = ref;
989
- refFn(ops.asElement(el));
1245
+ const attach = () => refFn(ops.asElement(ops.committed(el)));
1246
+ if (paintDepth > 0) pendingRefs.push(attach);
1247
+ else attach();
990
1248
  fiber.holes.push({ dispose: () => refFn(null) });
991
1249
  }
992
1250
  };
@@ -1015,7 +1273,7 @@ const createPainter = (ops) => {
1015
1273
  nodes: [host]
1016
1274
  };
1017
1275
  },
1018
- paintHole: (fiber, view) => {
1276
+ paintHole: (fiber, view) => commit(() => {
1019
1277
  if (view === KEEP) return;
1020
1278
  let frame = fiber.islandFrame;
1021
1279
  if (!frame) {
@@ -1036,15 +1294,18 @@ const createPainter = (ops) => {
1036
1294
  }
1037
1295
  cframe.i = 0;
1038
1296
  const list = Array.isArray(view) ? view : null;
1039
- if (list && list.length > 0 && list.every((v) => isVNode(v) && v.key !== null && v.key !== void 0)) {
1297
+ if (list && list.length > 0 && list.every((v) => isVNode(v) && isFunction(v.type) && v.key !== null && v.key !== void 0)) {
1040
1298
  paintFns.keyedPaintHole(fiber, list);
1299
+ sweepComponentSlots(fiber);
1300
+ sweepIslandSlots(fiber);
1041
1301
  return;
1042
1302
  }
1043
1303
  const kept = /* @__PURE__ */ new Set();
1044
1304
  for (const s of cframe.slots) if (s && !s.hole.closed) kept.add(s.hole);
1305
+ const keptIslands = new Set(frame.slots);
1045
1306
  const keep = [];
1046
1307
  for (const h of fiber.holes) {
1047
- if (h.holeFiber && kept.has(h.holeFiber)) {
1308
+ if (h.holeFiber && kept.has(h.holeFiber) || h.islandSlot && keptIslands.has(h.islandSlot)) {
1048
1309
  keep.push(h);
1049
1310
  continue;
1050
1311
  }
@@ -1061,11 +1322,12 @@ const createPainter = (ops) => {
1061
1322
  api.insert(fiber, fresh);
1062
1323
  }
1063
1324
  sweepComponentSlots(fiber);
1064
- }
1325
+ sweepIslandSlots(fiber);
1326
+ })
1065
1327
  };
1066
1328
  const materializeAtom = (view, fiber) => {
1067
1329
  const reused = findReusableAtomHost(fiber, view.atom);
1068
- if (reused) return [ops.createSlotHost(reused.host.getAttribute(SLOT_ATTR))];
1330
+ if (reused) return [ops.reuseNode(reused.host)];
1069
1331
  const { fiber: hole, nodes } = api.openHole(fiber);
1070
1332
  const [host] = nodes;
1071
1333
  if (!host) return nodes;
@@ -1114,9 +1376,16 @@ const createPainter = (ops) => {
1114
1376
  const existing = frame.slots[i];
1115
1377
  if (existing && existing.type === type && existing.host.isConnected && isFunction(existing.updateProps)) {
1116
1378
  existing.updateProps(props);
1117
- return [ops.asNode(existing.host)];
1379
+ return [ops.reuseNode(existing.host)];
1380
+ }
1381
+ if (existing) {
1382
+ const idx = fiber.holes.findIndex((h) => h.islandSlot === existing);
1383
+ if (idx === -1) existing.unmount?.();
1384
+ else {
1385
+ const [removed] = fiber.holes.splice(idx, 1);
1386
+ removed?.dispose();
1387
+ }
1118
1388
  }
1119
- existing?.unmount?.();
1120
1389
  const tag = isString(type[ISLAND_SLOT_TAG]) ? type[ISLAND_SLOT_TAG] : "div";
1121
1390
  const el = ops.createElement(tag);
1122
1391
  el.dataset.ilha = "";
@@ -1136,6 +1405,7 @@ const createPainter = (ops) => {
1136
1405
  handle?.unmount?.();
1137
1406
  if (frame.slots[i] === slot) frame.slots[i] = void 0;
1138
1407
  },
1408
+ islandSlot: slot,
1139
1409
  keepOnMorph: true
1140
1410
  });
1141
1411
  }
@@ -1158,7 +1428,7 @@ const createPainter = (ops) => {
1158
1428
  if (existing.type === type && !hole.closed) {
1159
1429
  hole.propsBox = { current: props };
1160
1430
  runSetup(hole, () => run(props));
1161
- return [ops.asNode(hole.root)];
1431
+ return [ops.reuseNode(hole.root)];
1162
1432
  }
1163
1433
  if (!hole.closed) {
1164
1434
  const idx = fiber.holes.findIndex((h) => h.holeFiber === hole);
@@ -1199,7 +1469,7 @@ const createPainter = (ops) => {
1199
1469
  if (reuse.propsBox) reuse.propsBox.current = props;
1200
1470
  const next = withFiber(fiber, () => run(reuse.propsBox?.current ?? props));
1201
1471
  if (next && !isThenable(next) && !isSetupFn(next)) reuse.paint(next);
1202
- return [ops.asNode(reuse.root)];
1472
+ return [ops.reuseNode(reuse.root)];
1203
1473
  }
1204
1474
  if (!k) return materializeUnkeyedComponent(run, type, props, fiber);
1205
1475
  const { fiber: khole, nodes: knodes } = api.openHole(fiber);
@@ -1275,11 +1545,11 @@ const createPainter = (ops) => {
1275
1545
  nodes.push(...made);
1276
1546
  }
1277
1547
  fiber.keyedHoles = next;
1278
- ops.clearRoot(fiber.root);
1279
- api.insert(fiber, nodes);
1548
+ ops.placeChildren(fiber.root, nodes);
1280
1549
  };
1281
1550
  return {
1282
1551
  applyProps: api.applyProps,
1552
+ commit,
1283
1553
  disposeHoles: (fiber, opts) => disposeHoles(fiber, opts),
1284
1554
  insert: api.insert,
1285
1555
  materialize: api.materialize,
@@ -1291,7 +1561,7 @@ const createPainter = (ops) => {
1291
1561
  //#region src/paint.ts
1292
1562
  const noopDisconnect = () => {};
1293
1563
  const core$1 = createPainter({
1294
- appendRoot: (root, node) => root.append(node),
1564
+ appendRoot: (root, node) => insertRendered(root, node, null),
1295
1565
  asElement: (el) => el,
1296
1566
  asNode: (host) => host,
1297
1567
  asRoot: (el) => el,
@@ -1299,6 +1569,7 @@ const core$1 = createPainter({
1299
1569
  clearRoot: (root) => {
1300
1570
  if (root instanceof Element) root.replaceChildren();
1301
1571
  },
1572
+ committed: committedElement,
1302
1573
  createElement: (tag, ns) => ns ? document.createElementNS(ns, tag) : document.createElement(tag),
1303
1574
  createRaw: (html, parent) => {
1304
1575
  const range = document.createRange();
@@ -1313,6 +1584,10 @@ const core$1 = createPainter({
1313
1584
  },
1314
1585
  createText: (text) => document.createTextNode(text),
1315
1586
  disconnect: noopDisconnect,
1587
+ placeChildren: (root, nodes) => {
1588
+ if (root instanceof Element) placeChildren(root, nodes);
1589
+ },
1590
+ reuseNode: (host) => host instanceof Element && host.isConnected ? standInFor(host) : host,
1316
1591
  setFormControl: (el, key, v) => {
1317
1592
  const node = el;
1318
1593
  if (key === "value") {
@@ -1371,7 +1646,7 @@ const paintHydrate = (fiber, view) => {
1371
1646
  root: el
1372
1647
  };
1373
1648
  el.replaceChildren();
1374
- for (const c of view.children) for (const n of core$1.materialize(c, childFiber)) el.append(n);
1649
+ for (const c of view.children) for (const n of core$1.materialize(c, childFiber)) insertRendered(el, n, null);
1375
1650
  };
1376
1651
  const paintMorphLive = (fiber, view) => {
1377
1652
  if (!fiber.liveEl) return;
@@ -1389,7 +1664,7 @@ const paintMorphLive = (fiber, view) => {
1389
1664
  refreshAllAtomHosts(fiber);
1390
1665
  pruneDisconnectedAtomHoles(fiber);
1391
1666
  };
1392
- const paint$1 = (fiber, view) => {
1667
+ const paintRoot = (fiber, view) => {
1393
1668
  fiber.islandFrame ??= {
1394
1669
  i: 0,
1395
1670
  slots: []
@@ -1420,6 +1695,11 @@ const paint$1 = (fiber, view) => {
1420
1695
  const first = fiber.root instanceof Element ? fiber.root.firstElementChild : null;
1421
1696
  if (first) fiber.liveEl = first;
1422
1697
  };
1698
+ const paint$1 = (fiber, view) => {
1699
+ core$1.commit(() => {
1700
+ paintRoot(fiber, view);
1701
+ });
1702
+ };
1423
1703
  const paintError$1 = (fiber, e) => {
1424
1704
  console.error(e);
1425
1705
  paint$1(fiber, errorView(e));
@@ -1642,6 +1922,7 @@ const ssrOps = {
1642
1922
  asRoot: (el) => ssrAs(el),
1643
1923
  bindEvents: bindSsrEvents,
1644
1924
  clearRoot: (root) => ssrAs(root).replaceChildren(),
1925
+ committed: (el) => el,
1645
1926
  createElement: createSsrElement,
1646
1927
  createRaw: (html, _parent) => [createSsrRaw(html)],
1647
1928
  createSlotHost: (slotId) => {
@@ -1654,6 +1935,12 @@ const ssrOps = {
1654
1935
  disconnect: (el) => {
1655
1936
  el.isConnected = false;
1656
1937
  },
1938
+ placeChildren: (root, nodes) => {
1939
+ const host = ssrAs(root);
1940
+ host.replaceChildren();
1941
+ for (const n of nodes) host.append(n);
1942
+ },
1943
+ reuseNode: (host) => ssrAs(host),
1657
1944
  setFormControl: (el, key, v) => {
1658
1945
  if (key === "value") el.setAttribute("value", String(v ?? ""));
1659
1946
  else if (v) el.setAttribute(key, "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilha",
3
- "version": "0.14.7",
3
+ "version": "0.14.9",
4
4
  "description": "Generator-first island UI on Effect",
5
5
  "keywords": [
6
6
  "effect",
@@ -64,7 +64,7 @@
64
64
  "typecheck": "tsc -p tsconfig.types.json --noEmit"
65
65
  },
66
66
  "devDependencies": {
67
- "@happy-dom/global-registrator": "^20.11.2",
67
+ "@happy-dom/global-registrator": "^20.14.5",
68
68
  "effect": "^4.0.0-rc.118"
69
69
  },
70
70
  "peerDependencies": {