ilha 0.9.3 → 0.9.5

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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/index.js +265 -42
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -420,6 +420,8 @@ const Card = ilha.render(
420
420
  );
421
421
  ```
422
422
 
423
+ Props (including JSX `children` and function callbacks) stay on the **live slot map** during parent render/mount. `data-ilha-props` only carries JSON-safe scalars for hydration hints — never rely on it for children or handlers. Nested islands under layout shells (`defineLayout` / `wrapLayout`) use the same path as page-level composition.
424
+
423
425
  **Keyed children** — use `.key()` when a child may reorder or appear conditionally. Keys must be unique within a parent render:
424
426
 
425
427
  ```ts
package/dist/index.js CHANGED
@@ -121,8 +121,96 @@ function isStableInlineSlotMount(initialHtml, renderedHtml, slotIds) {
121
121
  }
122
122
  return true;
123
123
  }
124
+ /**
125
+ * Props safe to embed in `data-ilha-props`.
126
+ * Functions cannot round-trip through JSON; children are owned by the live slot
127
+ * map (client) and/or the inlined SSR subtree — never depend on the attr for them.
128
+ * RawHtml is tagged so a rare attr-only mount can revive `Symbol.for("ilha.raw")`.
129
+ */
130
+ function slotPropsForAttr(props) {
131
+ if (props === void 0) return void 0;
132
+ let out;
133
+ for (const key of Object.keys(props)) {
134
+ if (key === "children") continue;
135
+ const value = props[key];
136
+ if (typeof value === "function" || typeof value === "symbol") continue;
137
+ const encoded = encodeSlotPropValue(value);
138
+ if (encoded === void 0 && value !== void 0 && value !== null) continue;
139
+ (out ??= {})[key] = encoded;
140
+ }
141
+ return out;
142
+ }
143
+ const SLOT_RAW = Symbol.for("ilha.raw");
144
+ function isRawHtmlValue(v) {
145
+ return !!(v && typeof v === "object" && SLOT_RAW in v);
146
+ }
147
+ function encodeSlotPropValue(value, seen) {
148
+ if (value == null) return value;
149
+ if (typeof value === "function" || typeof value === "symbol") return void 0;
150
+ if (typeof value !== "object") return value;
151
+ if (isRawHtmlValue(value)) return {
152
+ __ilha: "raw",
153
+ value: value.value
154
+ };
155
+ const visited = seen ?? /* @__PURE__ */ new WeakSet();
156
+ if (visited.has(value)) throw new TypeError("encodeSlotPropValue: circular reference in slot props");
157
+ visited.add(value);
158
+ if (Array.isArray(value)) return value.map((item) => encodeSlotPropValue(item, visited)).filter((item) => item !== void 0);
159
+ if (Object.getPrototypeOf(value) !== Object.prototype) return void 0;
160
+ const obj = value;
161
+ const encoded = {};
162
+ for (const key of Object.keys(obj)) {
163
+ const next = encodeSlotPropValue(obj[key], visited);
164
+ if (next !== void 0 || obj[key] === null) encoded[key] = next;
165
+ }
166
+ return encoded;
167
+ }
168
+ function makeRawHtml(value) {
169
+ return {
170
+ [SLOT_RAW]: true,
171
+ value
172
+ };
173
+ }
174
+ function reviveSlotPropValue(value) {
175
+ if (value == null || typeof value !== "object") return value;
176
+ if (Array.isArray(value)) return value.map(reviveSlotPropValue);
177
+ const obj = value;
178
+ if (obj.__ilha === "raw" && typeof obj.value === "string") return makeRawHtml(obj.value);
179
+ const out = {};
180
+ for (const key of Object.keys(obj)) out[key] = reviveSlotPropValue(obj[key]);
181
+ return out;
182
+ }
183
+ /**
184
+ * Exact legacy shape produced when RawHtml lost its brand under JSON.stringify:
185
+ * a plain object with a single own key `value` that is a string.
186
+ */
187
+ function isLegacyRawHtmlBlob(value) {
188
+ if (!value || typeof value !== "object" || isRawHtmlValue(value)) return false;
189
+ if (Object.getPrototypeOf(value) !== Object.prototype) return false;
190
+ const keys = Object.keys(value);
191
+ return keys.length === 1 && keys[0] === "value" && typeof value.value === "string";
192
+ }
193
+ /** Revive attr-parsed props (RawHtml tags). Children stay as plain data if present. */
194
+ function reviveSlotProps(props) {
195
+ if (typeof props !== "object" || props === null || Array.isArray(props)) return {};
196
+ const out = reviveSlotPropValue(props);
197
+ if ("children" in out) out.children = reviveLegacyChildren(out.children);
198
+ return out;
199
+ }
200
+ function reviveLegacyChildren(children) {
201
+ if (!Array.isArray(children)) {
202
+ if (isLegacyRawHtmlBlob(children)) return makeRawHtml(children.value);
203
+ return reviveSlotPropValue(children);
204
+ }
205
+ return children.map((child) => {
206
+ if (isLegacyRawHtmlBlob(child)) return makeRawHtml(child.value);
207
+ return reviveSlotPropValue(child);
208
+ });
209
+ }
210
+ /** Serialized form of slot props, matching the decoded `data-ilha-props` attr. */
124
211
  function serializeSlotProps(props) {
125
- return props === void 0 ? "" : JSON.stringify(props);
212
+ const safe = slotPropsForAttr(props);
213
+ return safe === void 0 ? "" : JSON.stringify(safe);
126
214
  }
127
215
  const MORPH_CONTROLLER_SLOT_SELECTOR = "[data-slot]";
128
216
  const MORPH_CONTROLLER_ATTRS = /* @__PURE__ */ new Set([
@@ -138,7 +226,19 @@ const MORPH_CONTROLLER_ATTRS = /* @__PURE__ */ new Set([
138
226
  "data-selected",
139
227
  "data-panel-open"
140
228
  ]);
229
+ /**
230
+ * Opt-in contract for controller-owned attributes: an element carrying
231
+ * `data-morph-preserve="attr-a attr-b class"` keeps those attributes exactly
232
+ * as they are in the live DOM — the morph neither overwrites nor removes them.
233
+ * The Areia list above stays as a built-in default for `[data-slot]` elements.
234
+ */
235
+ const MORPH_PRESERVE_ATTR = "data-morph-preserve";
141
236
  function shouldPreserveMorphAttr(el, name) {
237
+ if (name === MORPH_PRESERVE_ATTR) return el.hasAttribute(MORPH_PRESERVE_ATTR);
238
+ const custom = el.getAttribute(MORPH_PRESERVE_ATTR);
239
+ if (custom !== null) {
240
+ for (const token of custom.split(/\s+/)) if (token === name) return true;
241
+ }
142
242
  return el.matches(MORPH_CONTROLLER_SLOT_SELECTOR) && MORPH_CONTROLLER_ATTRS.has(name);
143
243
  }
144
244
  function syncAttributes(from, to) {
@@ -151,18 +251,30 @@ function syncAttributes(from, to) {
151
251
  if (!to.hasAttribute(name)) from.removeAttribute(name);
152
252
  }
153
253
  }
254
+ /**
255
+ * Morph identity key: explicit `data-key`, or the child-island slot id.
256
+ * Slot hosts are matched by id like keyed list items so a mounted child
257
+ * island's host element is reconciled IN PLACE on parent re-renders — never
258
+ * detached and reinserted, which would blur a focused element inside it.
259
+ */
260
+ function morphKeyOf(el) {
261
+ const k = el.getAttribute("data-key");
262
+ if (k !== null) return `k:${k}`;
263
+ const s = el.getAttribute(SLOT_ATTR);
264
+ return s === null ? null : `s:${s}`;
265
+ }
154
266
  function morphChildren(fromParent, toParent) {
155
267
  const toNodes = Array.from(toParent.childNodes);
156
268
  let fromKeyed = null;
157
269
  for (const child of fromParent.children) {
158
- const k = child.getAttribute("data-key");
270
+ const k = morphKeyOf(child);
159
271
  if (k !== null && !(fromKeyed ??= /* @__PURE__ */ new Map()).has(k)) fromKeyed.set(k, child);
160
272
  }
161
273
  let toKeys = null;
162
274
  if (fromKeyed !== null) {
163
275
  toKeys = /* @__PURE__ */ new Set();
164
276
  for (const child of toParent.children) {
165
- const k = child.getAttribute("data-key");
277
+ const k = morphKeyOf(child);
166
278
  if (k !== null) toKeys.add(k);
167
279
  }
168
280
  }
@@ -170,7 +282,7 @@ function morphChildren(fromParent, toParent) {
170
282
  const toNode = toNodes[i];
171
283
  let fromNode = fromParent.childNodes[i];
172
284
  if (fromKeyed !== null) {
173
- const toKey = toNode.nodeType === 1 ? toNode.getAttribute("data-key") : null;
285
+ const toKey = toNode.nodeType === 1 ? morphKeyOf(toNode) : null;
174
286
  if (toKey !== null) {
175
287
  const match = fromKeyed.get(toKey);
176
288
  if (match) {
@@ -182,7 +294,7 @@ function morphChildren(fromParent, toParent) {
182
294
  }
183
295
  }
184
296
  if (fromNode instanceof Element) {
185
- const fromKey = fromNode.getAttribute("data-key");
297
+ const fromKey = morphKeyOf(fromNode);
186
298
  if (fromKey !== null && fromKey !== toKey && toKeys.has(fromKey)) {
187
299
  fromParent.insertBefore(toNode.cloneNode(true), fromNode);
188
300
  continue;
@@ -208,6 +320,18 @@ function morphChildren(fromParent, toParent) {
208
320
  fromParent.replaceChild(toEl.cloneNode(true), fromEl);
209
321
  continue;
210
322
  }
323
+ if (fromEl.hasAttribute(LEAVING_ATTR)) {
324
+ fromParent.replaceChild(toEl.cloneNode(true), fromEl);
325
+ continue;
326
+ }
327
+ {
328
+ const slotId = toEl.getAttribute(SLOT_ATTR);
329
+ if (slotId !== null && fromEl.getAttribute(SLOT_ATTR) === slotId) {
330
+ const props = toEl.getAttribute(PROPS_ATTR);
331
+ if (props !== null && fromEl.getAttribute(PROPS_ATTR) !== props) fromEl.setAttribute(PROPS_ATTR, props);
332
+ continue;
333
+ }
334
+ }
211
335
  if (fromEl.localName === "input" && fromEl.type !== toEl.type) {
212
336
  fromParent.replaceChild(toEl.cloneNode(true), fromEl);
213
337
  continue;
@@ -222,6 +346,22 @@ function morphChildren(fromParent, toParent) {
222
346
  if (newValue !== hadValue) fromEl.value = newValue ?? "";
223
347
  continue;
224
348
  }
349
+ if (fromEl.localName === "select") {
350
+ const before = /* @__PURE__ */ new Map();
351
+ for (const o of fromEl.options) before.set(o, {
352
+ attr: o.hasAttribute("selected"),
353
+ live: o.selected
354
+ });
355
+ syncAttributes(fromEl, toEl);
356
+ morphChildren(fromEl, toEl);
357
+ const options = Array.from(fromEl.options);
358
+ if (options.some((o) => o.hasAttribute("selected") !== (before.get(o)?.attr ?? false))) for (const o of options) o.selected = o.hasAttribute("selected");
359
+ else for (const o of options) {
360
+ const prev = before.get(o);
361
+ if (prev && o.selected !== prev.live) o.selected = prev.live;
362
+ }
363
+ continue;
364
+ }
225
365
  syncAttributes(fromEl, toEl);
226
366
  if (fromEl.localName === "textarea") {
227
367
  const newText = toEl.textContent ?? "";
@@ -234,9 +374,62 @@ function morphChildren(fromParent, toParent) {
234
374
  }
235
375
  while (fromParent.childNodes.length > toNodes.length) fromParent.lastChild.remove();
236
376
  }
377
+ /**
378
+ * Invariant: the morph must never lose focus for an element that survives it.
379
+ * Surviving slot hosts and keyed elements are patched in place, but a genuine
380
+ * reorder still detaches-and-reinserts an ancestor of `document.activeElement`,
381
+ * which blurs it in real engines — snapshot before, restore after.
382
+ */
383
+ function snapshotFocusForMorph() {
384
+ if (typeof document === "undefined") return null;
385
+ const active = document.activeElement;
386
+ if (!(active instanceof HTMLElement) || active === document.body) return null;
387
+ let selection = null;
388
+ let range = null;
389
+ try {
390
+ if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) selection = {
391
+ start: active.selectionStart,
392
+ end: active.selectionEnd,
393
+ dir: active.selectionDirection
394
+ };
395
+ else if (active.isContentEditable) {
396
+ const sel = window.getSelection();
397
+ if (sel && sel.rangeCount > 0) range = sel.getRangeAt(0).cloneRange();
398
+ }
399
+ } catch {}
400
+ return {
401
+ active,
402
+ selection,
403
+ range
404
+ };
405
+ }
406
+ function restoreFocusAfterMorph(snapshot) {
407
+ if (!snapshot) return;
408
+ const { active, selection, range } = snapshot;
409
+ if (!active.isConnected) return;
410
+ try {
411
+ if (document.activeElement !== active) active.focus({ preventScroll: true });
412
+ if (selection && selection.start !== null) {
413
+ const el = active;
414
+ if (el.selectionStart !== selection.start || el.selectionEnd !== selection.end) el.setSelectionRange(selection.start, selection.end, selection.dir ?? "none");
415
+ }
416
+ if (range && range.startContainer.isConnected) {
417
+ const sel = window.getSelection();
418
+ if (sel) {
419
+ const current = sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
420
+ if (!(current !== null && current.startContainer === range.startContainer && current.startOffset === range.startOffset && current.endContainer === range.endContainer && current.endOffset === range.endOffset)) {
421
+ sel.removeAllRanges();
422
+ sel.addRange(range);
423
+ }
424
+ }
425
+ }
426
+ } catch {}
427
+ }
237
428
  function morphInner(from, to) {
238
429
  if (from.localName !== to.localName || from.namespaceURI !== to.namespaceURI) throw new Error("[ilha] morph: elements must match");
430
+ const focus = snapshotFocusForMorph();
239
431
  morphChildren(from, to);
432
+ restoreFocusAfterMorph(focus);
240
433
  }
241
434
  function isStandardSchema(value) {
242
435
  if (value == null || typeof value !== "object") return false;
@@ -283,13 +476,22 @@ const ISLAND_SLOT_TAG = Symbol.for("ilha.islandSlotTag");
283
476
  * on unmount. Not part of the public surface. */
284
477
  const ISLAND_MOUNT_HANDLES = /* @__PURE__ */ new WeakMap();
285
478
  const SLOT_ATTR = "data-ilha-slot";
479
+ /** Marks a slot host whose child island is mid leave-transition: it has been
480
+ * unmounted (and its slot id stripped) but stays connected so the leave
481
+ * animation can paint. The morph replaces such elements instead of matching
482
+ * or mutating them. */
483
+ const LEAVING_ATTR = "data-ilha-leaving";
286
484
  const PROPS_ATTR = "data-ilha-props";
287
485
  const STATE_ATTR = "data-ilha-state";
288
486
  const CSS_ATTR = "data-ilha-css";
289
487
  function buildScopedStyle(css) {
290
488
  return `<style ${CSS_ATTR}>@scope (:scope) to ([data-ilha]){${css.replace(/<\/style/gi, "<\\/style")}}</style>`;
291
489
  }
292
- const renderCtxStack = [];
490
+ const RENDER_CTX_STACK = Symbol.for("ilha.renderCtxStack");
491
+ function renderCtxStack() {
492
+ const g = globalThis;
493
+ return g[RENDER_CTX_STACK] ??= [];
494
+ }
293
495
  function pushRenderCtx(liveHost, asyncChildren) {
294
496
  const ctx = {
295
497
  slots: /* @__PURE__ */ new Map(),
@@ -298,14 +500,15 @@ function pushRenderCtx(liveHost, asyncChildren) {
298
500
  pending: asyncChildren ? /* @__PURE__ */ new Map() : void 0,
299
501
  binds: []
300
502
  };
301
- renderCtxStack.push(ctx);
503
+ renderCtxStack().push(ctx);
302
504
  return ctx;
303
505
  }
304
506
  function popRenderCtx() {
305
- renderCtxStack.pop();
507
+ renderCtxStack().pop();
306
508
  }
307
509
  function currentRenderCtx() {
308
- return renderCtxStack[renderCtxStack.length - 1];
510
+ const stack = renderCtxStack();
511
+ return stack[stack.length - 1];
309
512
  }
310
513
  function isIsland(v) {
311
514
  return typeof v === "function" && ISLAND in v;
@@ -325,7 +528,8 @@ function emitIslandSlot(island, props, key) {
325
528
  props
326
529
  });
327
530
  const slotTag = getIslandSlotTag(island);
328
- const propsAttr = props ? ` ${PROPS_ATTR}='${escapeHtml(JSON.stringify(props))}'` : "";
531
+ const attrProps = slotPropsForAttr(props);
532
+ const propsAttr = attrProps ? ` ${PROPS_ATTR}='${escapeHtml(JSON.stringify(attrProps))}'` : "";
329
533
  if (ctx?.liveHost) return wrapIslandSlotHtml(slotTag, id, propsAttr, "");
330
534
  if (ctx?.pending) {
331
535
  popRenderCtx();
@@ -337,7 +541,7 @@ function emitIslandSlot(island, props, key) {
337
541
  }
338
542
  return wrapIslandSlotHtml(slotTag, id, propsAttr, String(result));
339
543
  } finally {
340
- renderCtxStack.push(ctx);
544
+ renderCtxStack().push(ctx);
341
545
  }
342
546
  }
343
547
  const inner = island.toString(props);
@@ -1428,7 +1632,7 @@ var IlhaBuilder = class IlhaBuilder {
1428
1632
  const rawProps = host.getAttribute(PROPS_ATTR);
1429
1633
  if (rawProps) {
1430
1634
  const parsed = safeParseSnapshot(rawProps, PROPS_ATTR);
1431
- if (parsed !== void 0) props = parsed;
1635
+ if (parsed !== void 0) props = reviveSlotProps(parsed);
1432
1636
  }
1433
1637
  }
1434
1638
  const inputSignal = signal(resolveInput(props));
@@ -1521,6 +1725,8 @@ var IlhaBuilder = class IlhaBuilder {
1521
1725
  leavingSlots.delete(id);
1522
1726
  };
1523
1727
  if (result instanceof Promise) {
1728
+ entry.el.removeAttribute(SLOT_ATTR);
1729
+ entry.el.setAttribute(LEAVING_ATTR, "");
1524
1730
  leavingSlots.set(id, {
1525
1731
  el: entry.el,
1526
1732
  token
@@ -1548,6 +1754,13 @@ var IlhaBuilder = class IlhaBuilder {
1548
1754
  }
1549
1755
  return index;
1550
1756
  }
1757
+ /** Push live props from a fresh slot map into already-mounted children. */
1758
+ function pushUpdatedProps(nextSlots) {
1759
+ for (const [id, entry] of mountedSlots) {
1760
+ const next = nextSlots.get(id);
1761
+ if (next) entry.updateProps(next.props);
1762
+ }
1763
+ }
1551
1764
  function mountSlots(slotMap) {
1552
1765
  for (const [id, entry] of mountedSlots) if (!slotMap.has(id)) teardownMountedSlot(id, entry);
1553
1766
  for (const [id, entry] of mountedSlots) {
@@ -1569,7 +1782,7 @@ var IlhaBuilder = class IlhaBuilder {
1569
1782
  const rawProps = slotEl.getAttribute(PROPS_ATTR) ?? slotEl.getAttribute("data-props");
1570
1783
  if (rawProps) {
1571
1784
  const parsed = safeParseSnapshot(rawProps, `props on [${SLOT_ATTR}="${id}"]`);
1572
- if (parsed !== void 0) slotProps = parsed;
1785
+ if (parsed !== void 0) slotProps = reviveSlotProps(parsed);
1573
1786
  }
1574
1787
  }
1575
1788
  const prevSub = setActiveSub(void 0);
@@ -1747,6 +1960,7 @@ var IlhaBuilder = class IlhaBuilder {
1747
1960
  cleanups.push(stopDerived);
1748
1961
  let initialized = false;
1749
1962
  const initialRenderedHtml = initial.html;
1963
+ let lastRendered = null;
1750
1964
  let renderEpoch = 0;
1751
1965
  const stopRender = effect(() => {
1752
1966
  const epoch = ++renderEpoch;
@@ -1759,14 +1973,24 @@ var IlhaBuilder = class IlhaBuilder {
1759
1973
  if (!initialized) {
1760
1974
  initialized = true;
1761
1975
  if (preserveSSRDom) {
1762
- for (const [id, entry] of mountedSlots) {
1763
- const next = newSlotMap.get(id);
1764
- if (next) entry.updateProps(next.props);
1976
+ pushUpdatedProps(newSlotMap);
1977
+ if (rendered === initialRenderedHtml) {
1978
+ lastRendered = rendered;
1979
+ return;
1765
1980
  }
1766
- if (rendered === initialRenderedHtml) return;
1767
1981
  }
1768
- if (rendered === initialRenderedHtml) return;
1769
- if (mountedSlots.size > 0 && mountedSlots.size === newSlotMap.size && [...newSlotMap.keys()].every((id) => mountedSlots.has(id)) && isStableInlineSlotMount(initialRenderedHtml, rendered, newSlotMap.keys())) return;
1982
+ if (rendered === initialRenderedHtml) {
1983
+ lastRendered = rendered;
1984
+ return;
1985
+ }
1986
+ if (mountedSlots.size > 0 && mountedSlots.size === newSlotMap.size && [...newSlotMap.keys()].every((id) => mountedSlots.has(id)) && isStableInlineSlotMount(initialRenderedHtml, rendered, newSlotMap.keys())) {
1987
+ pushUpdatedProps(newSlotMap);
1988
+ return;
1989
+ }
1990
+ }
1991
+ if (rendered === lastRendered && mountedSlots.size === newSlotMap.size && [...newSlotMap].every(([id, next]) => mountedSlots.get(id)?.island === next.island)) {
1992
+ pushUpdatedProps(newSlotMap);
1993
+ return;
1770
1994
  }
1771
1995
  detachListeners();
1772
1996
  stopBindings();
@@ -1785,34 +2009,33 @@ var IlhaBuilder = class IlhaBuilder {
1785
2009
  }
1786
2010
  const applyMorph = () => {
1787
2011
  if (epoch !== renderEpoch) return;
1788
- const preserved = /* @__PURE__ */ new Map();
1789
- const listShrunk = newSlotMap.size < prevMountedCount;
1790
- for (const [id, entry] of mountedSlots) {
1791
- if (!newSlotMap.has(id)) continue;
1792
- if (!entry.el.isConnected) continue;
1793
- if (listShrunk && id.startsWith("p:")) {
1794
- teardownMountedSlot(id, entry);
1795
- continue;
2012
+ if (newSlotMap.size < prevMountedCount) {
2013
+ let divergeFrom = Infinity;
2014
+ for (const [id, entry] of mountedSlots) {
2015
+ if (!id.startsWith("p:")) continue;
2016
+ const next = newSlotMap.get(id);
2017
+ if (!next) continue;
2018
+ const index = Number(id.slice(2));
2019
+ if (Number.isNaN(index) || index >= divergeFrom) continue;
2020
+ if (serializeSlotProps(next.props) !== (entry.el.getAttribute(PROPS_ATTR) ?? "")) divergeFrom = index;
2021
+ }
2022
+ for (const [id, entry] of mountedSlots) {
2023
+ if (!newSlotMap.has(id)) continue;
2024
+ if (!entry.el.isConnected) continue;
2025
+ if (id.startsWith("p:") && Number(id.slice(2)) >= divergeFrom) teardownMountedSlot(id, entry);
1796
2026
  }
1797
- const stubTag = entry.el.tagName.toLowerCase();
1798
- const stub = document.createElement(stubTag);
1799
- stub.setAttribute(SLOT_ATTR, id);
1800
- const incomingProps = serializeSlotProps(newSlotMap.get(id)?.props);
1801
- if (incomingProps !== "") stub.setAttribute(PROPS_ATTR, incomingProps);
1802
- entry.el.replaceWith(stub);
1803
- preserved.set(id, entry.el);
1804
2027
  }
1805
2028
  const tpl = document.createElement("template");
1806
2029
  const morphRootTag = host.tagName.toLowerCase();
1807
2030
  tpl.innerHTML = `<${morphRootTag}>${html}</${morphRootTag}>`;
1808
2031
  morphInner(host, tpl.content.firstElementChild);
1809
- if (preserved.size > 0) {
1810
- const rehomeIndex = buildSlotIndex();
1811
- for (const [id, liveEl] of preserved) {
1812
- const stub = rehomeIndex.get(id);
1813
- if (!stub) continue;
1814
- stub.replaceWith(liveEl);
1815
- }
2032
+ lastRendered = rendered;
2033
+ let rehomeIndex = null;
2034
+ for (const [id, entry] of mountedSlots) {
2035
+ if (!newSlotMap.has(id) || entry.el.isConnected) continue;
2036
+ rehomeIndex ??= buildSlotIndex();
2037
+ const stub = rehomeIndex.get(id);
2038
+ if (stub && stub !== entry.el) stub.replaceWith(entry.el);
1816
2039
  }
1817
2040
  attachListeners();
1818
2041
  stopBindings = applyTemplateBindings(host, newBinds);
@@ -2072,7 +2295,7 @@ function mountAll(registry, options = {}) {
2072
2295
  const rawProps = host.getAttribute(PROPS_ATTR);
2073
2296
  if (rawProps) {
2074
2297
  const parsed = safeParseSnapshot(rawProps, `${PROPS_ATTR} on [data-ilha="${name}"]`);
2075
- if (parsed !== void 0) props = parsed;
2298
+ if (parsed !== void 0) props = reviveSlotProps(parsed);
2076
2299
  }
2077
2300
  unmounts.push(island.mount(host, props));
2078
2301
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilha",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "A tiny, framework-free island architecture library",
5
5
  "keywords": [
6
6
  "framework-free",