ilha 0.6.0 → 0.6.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.d.ts CHANGED
@@ -51,6 +51,7 @@ interface IslandCall {
51
51
  interface MarkedSignalAccessor<T> {
52
52
  (): T;
53
53
  (...args: [value: T]): void;
54
+ select<S>(selector: (state: T) => S): MarkedSignalAccessor<S>;
54
55
  [SIGNAL_ACCESSOR]: true;
55
56
  }
56
57
  declare function ilhaRaw(value: string): RawHtml;
@@ -58,11 +59,11 @@ declare function ilhaCss(strings: TemplateStringsArray | string, ...values: (str
58
59
  type JsxChild = unknown;
59
60
  type JsxProps = Record<string, unknown> | null | undefined;
60
61
  type JsxType = string | ((props: Record<string, unknown>) => unknown);
61
- declare function ilhaJsx(type: JsxType, props: JsxProps, ...children: JsxChild[]): RawHtml;
62
+ declare function ilhaJsx(type: JsxType, props: JsxProps, maybeKey?: JsxChild | string | number, ...restChildren: JsxChild[]): RawHtml;
62
63
  declare function ilhaFragment(props: {
63
64
  children?: JsxChild;
64
65
  } | null, ...children: JsxChild[]): RawHtml;
65
- declare function ilhaJsxDEV(type: JsxType, props: JsxProps): RawHtml;
66
+ declare function ilhaJsxDEV(type: JsxType, props: JsxProps, maybeKey?: string | number, _source?: unknown, _self?: unknown): RawHtml;
66
67
  declare function ilhaHtml(strings: TemplateStringsArray, ...values: unknown[]): RawHtml;
67
68
  type ContextSignal<T> = {
68
69
  (): T;
@@ -80,7 +81,7 @@ declare function ilhaContext<T>(key: string, initial: T): ContextSignal<T>;
80
81
  * or `.render()` automatically subscribes the surrounding reactive scope —
81
82
  * so when the signal changes, dependents re-run as if it were local state.
82
83
  */
83
- declare function ilhaSignal<T>(initial: T): ExternalSignal<T>;
84
+ declare function ilhaSignal<T>(initial: T): SignalAccessor<T>;
84
85
  /**
85
86
  * Run `fn` with reactive tracking suspended. Reading signals inside `fn`
86
87
  * returns their current value without subscribing the surrounding scope.
@@ -115,10 +116,7 @@ interface DerivedEntry<TInput, TStateMap extends Record<string, unknown>> {
115
116
  fn: DerivedFn<TInput, TStateMap, unknown>;
116
117
  }
117
118
  type IslandDerived<TDerivedMap extends Record<string, unknown>> = { readonly [K in keyof TDerivedMap]: DerivedValue<TDerivedMap[K]> };
118
- interface ExternalSignal<T = unknown> {
119
- (): T;
120
- (value: T): void;
121
- }
119
+ type ExternalSignal<T = unknown> = SignalAccessor<T>;
122
120
  type SignalAccessor<T> = MarkedSignalAccessor<T>;
123
121
  type MergeState<TStateMap extends Record<string, unknown>, K extends string, V> = Omit<TStateMap, K> & Record<K, V>;
124
122
  type IslandState<TStateMap extends Record<string, unknown>> = { readonly [K in keyof TStateMap]-?: SignalAccessor<TStateMap[K]> };
@@ -243,7 +241,7 @@ interface MountOptions {
243
241
  lazy?: boolean;
244
242
  }
245
243
  interface MountResult {
246
- unmount: () => void;
244
+ unmount: () => void | Promise<void>;
247
245
  }
248
246
  interface BuilderConfig<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>> {
249
247
  schema: StandardSchemaV1 | null;
package/dist/index.js CHANGED
@@ -16,9 +16,76 @@ function shallowEqualInput(a, b) {
16
16
  }
17
17
  return true;
18
18
  }
19
+ function extractDirectChildSlotIdsInOrder(html) {
20
+ if (typeof document === "undefined") return [];
21
+ const tpl = document.createElement("template");
22
+ tpl.innerHTML = html;
23
+ const root = tpl.content;
24
+ const ids = [];
25
+ for (const el of root.querySelectorAll("[data-ilha-slot]")) {
26
+ if (!(el instanceof Element)) continue;
27
+ let parent = el.parentElement ?? el.parentNode;
28
+ let nested = false;
29
+ while (parent && parent !== root) {
30
+ if (parent instanceof Element && parent.hasAttribute("data-ilha-slot")) {
31
+ nested = true;
32
+ break;
33
+ }
34
+ parent = parent instanceof Element ? parent.parentElement ?? parent.parentNode : parent.parentNode;
35
+ }
36
+ if (!nested) ids.push(el.getAttribute("data-ilha-slot"));
37
+ }
38
+ return ids;
39
+ }
40
+ function extractSlotPropsAttr(html, slotId) {
41
+ const escaped = slotId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
42
+ const withProps = new RegExp(`<div\\s[^>]*data-ilha-slot="${escaped}"[^>]*data-ilha-props='([^']*)'`);
43
+ const m = html.match(withProps);
44
+ if (m) return m[1];
45
+ const withoutProps = new RegExp(`<div\\s[^>]*data-ilha-slot="${escaped}"`);
46
+ return html.match(withoutProps) ? "" : null;
47
+ }
48
+ function isStableInlineSlotMount(initialHtml, renderedHtml, slotIds) {
49
+ const initialOrder = extractDirectChildSlotIdsInOrder(initialHtml);
50
+ const renderedOrder = extractDirectChildSlotIdsInOrder(renderedHtml);
51
+ if (initialOrder.length !== renderedOrder.length || initialOrder.some((id, i) => id !== renderedOrder[i])) return false;
52
+ for (const id of slotIds) {
53
+ const initialProps = extractSlotPropsAttr(initialHtml, id);
54
+ const renderedProps = extractSlotPropsAttr(renderedHtml, id);
55
+ if (initialProps === null || renderedProps === null) return false;
56
+ if (initialProps !== renderedProps) return false;
57
+ }
58
+ return true;
59
+ }
60
+ function serializeSlotProps(props) {
61
+ return props === void 0 ? "" : JSON.stringify(props);
62
+ }
63
+ const MORPH_CONTROLLER_SLOT_SELECTOR = "[data-slot]";
64
+ const MORPH_CONTROLLER_ATTRS = new Set([
65
+ "data-checked",
66
+ "data-unchecked",
67
+ "data-indeterminate",
68
+ "aria-checked",
69
+ "data-open",
70
+ "data-closed",
71
+ "data-state",
72
+ "aria-expanded",
73
+ "aria-hidden",
74
+ "data-selected",
75
+ "data-panel-open"
76
+ ]);
77
+ function shouldPreserveMorphAttr(el, name) {
78
+ return el.matches(MORPH_CONTROLLER_SLOT_SELECTOR) && MORPH_CONTROLLER_ATTRS.has(name);
79
+ }
19
80
  function syncAttributes(from, to) {
20
- for (const { name, value } of to.attributes) if (from.getAttribute(name) !== value) from.setAttribute(name, value);
21
- for (const { name } of Array.from(from.attributes)) if (!to.hasAttribute(name)) from.removeAttribute(name);
81
+ for (const { name, value } of to.attributes) {
82
+ if (shouldPreserveMorphAttr(from, name)) continue;
83
+ if (from.getAttribute(name) !== value) from.setAttribute(name, value);
84
+ }
85
+ for (const { name } of Array.from(from.attributes)) {
86
+ if (shouldPreserveMorphAttr(from, name)) continue;
87
+ if (!to.hasAttribute(name)) from.removeAttribute(name);
88
+ }
22
89
  }
23
90
  function morphChildren(fromParent, toParent) {
24
91
  const fromNodes = Array.from(fromParent.childNodes);
@@ -88,14 +155,14 @@ function dedentString(str) {
88
155
  const indent = Math.min(...lines.filter((l) => l.trim() !== "").map((l) => l.match(/^(\s*)/)[1].length));
89
156
  return lines.map((l) => l.slice(indent)).join("\n");
90
157
  }
91
- const RAW = Symbol("ilha.raw");
92
- const SIGNAL_ACCESSOR = Symbol("ilha.signalAccessor");
93
- const ISLAND = Symbol("ilha.island");
94
- const ISLAND_CALL = Symbol("ilha.islandCall");
158
+ const RAW = Symbol.for("ilha.raw");
159
+ const SIGNAL_ACCESSOR = Symbol.for("ilha.signalAccessor");
160
+ const ISLAND = Symbol.for("ilha.island");
161
+ const ISLAND_CALL = Symbol.for("ilha.islandCall");
95
162
  /** @internal Internal hook used by a parent's mountSlots to mount a child island and
96
163
  * retain a handle to push updated props into it on subsequent parent
97
164
  * re-renders. Not part of the public surface. */
98
- const ISLAND_MOUNT_INTERNAL = Symbol("ilha.islandMountInternal");
165
+ const ISLAND_MOUNT_INTERNAL = Symbol.for("ilha.islandMountInternal");
99
166
  const SLOT_ATTR = "data-ilha-slot";
100
167
  const PROPS_ATTR = "data-ilha-props";
101
168
  const STATE_ATTR = "data-ilha-state";
@@ -122,10 +189,19 @@ function currentRenderCtx() {
122
189
  return renderCtxStack[renderCtxStack.length - 1];
123
190
  }
124
191
  function isIsland(v) {
125
- return typeof v === "function" && ISLAND in v;
192
+ if (typeof v !== "function") return false;
193
+ if (ISLAND in v) return true;
194
+ return Object.getOwnPropertySymbols(v).some((s) => s.description === "ilha.island");
126
195
  }
127
196
  function isIslandCall(v) {
128
- return (typeof v === "object" || typeof v === "function") && v !== null && ISLAND_CALL in v;
197
+ if (v == null || typeof v !== "object" && typeof v !== "function") return false;
198
+ if (ISLAND_CALL in v) return true;
199
+ if (Object.getOwnPropertySymbols(v).some((s) => s.description === "ilha.islandCall")) return true;
200
+ if (typeof v === "object" && "island" in v) {
201
+ const island = v.island;
202
+ return typeof island === "function" && (ISLAND in island || Object.getOwnPropertySymbols(island).some((s) => s.description === "ilha.island"));
203
+ }
204
+ return false;
129
205
  }
130
206
  function emitIslandSlot(island, props, key) {
131
207
  const ctx = currentRenderCtx();
@@ -171,10 +247,86 @@ async function resolveAsyncChildren(html, ctx) {
171
247
  }
172
248
  function markSignalAccessor(fn) {
173
249
  fn[SIGNAL_ACCESSOR] = true;
174
- return fn;
250
+ const accessor = fn;
251
+ accessor.select = (selector) => createSelectAccessor(accessor, selector);
252
+ return accessor;
175
253
  }
176
254
  function isSignalAccessor(v) {
177
- return typeof v === "function" && SIGNAL_ACCESSOR in v;
255
+ if (typeof v !== "function") return false;
256
+ if (SIGNAL_ACCESSOR in v) return true;
257
+ return Object.getOwnPropertySymbols(v).some((s) => s.description === "ilha.signalAccessor");
258
+ }
259
+ function getAtPath(obj, path) {
260
+ let cur = obj;
261
+ for (const seg of path) {
262
+ if (cur == null) return void 0;
263
+ cur = cur[seg];
264
+ }
265
+ return cur;
266
+ }
267
+ function setAtPath(obj, path, value) {
268
+ if (path.length === 0) return value;
269
+ const [head, ...rest] = path;
270
+ if (Array.isArray(obj)) {
271
+ const idx = head;
272
+ if (idx < 0 || idx >= obj.length) return obj;
273
+ const next = obj[idx];
274
+ const updated = rest.length === 0 ? value : setAtPath(next, rest, value);
275
+ if (Object.is(next, updated)) return obj;
276
+ const copy = obj.slice();
277
+ copy[idx] = updated;
278
+ return copy;
279
+ }
280
+ if (obj !== null && typeof obj === "object") {
281
+ const record = obj;
282
+ const key = String(head);
283
+ const next = record[key];
284
+ const updated = rest.length === 0 ? value : setAtPath(next, rest, value);
285
+ if (rest.length === 0) {
286
+ if (Object.is(next, value)) return obj;
287
+ } else if (Object.is(next, updated)) return obj;
288
+ return {
289
+ ...record,
290
+ [key]: updated
291
+ };
292
+ }
293
+ return rest.length === 0 ? value : setAtPath(void 0, rest, value);
294
+ }
295
+ function toPathSegment(prop) {
296
+ if (typeof prop === "symbol") return null;
297
+ if (prop === "length") return null;
298
+ if (/^\d+$/.test(prop)) return Number(prop);
299
+ return prop;
300
+ }
301
+ function trackSelectPath(rootState, selector) {
302
+ const path = [];
303
+ const track = (value) => {
304
+ if (value === null || typeof value !== "object" && typeof value !== "function") return value;
305
+ return new Proxy(value, { get(target, prop, receiver) {
306
+ const seg = toPathSegment(prop);
307
+ if (seg != null) path.push(seg);
308
+ const next = Reflect.get(target, prop, receiver);
309
+ return seg != null && next !== null && typeof next === "object" ? track(next) : next;
310
+ } });
311
+ };
312
+ selector(track(rootState));
313
+ return path;
314
+ }
315
+ function createSelectAccessor(root, selector) {
316
+ const path = trackSelectPath(root(), selector);
317
+ const selected = selector(root());
318
+ const resolved = path.length === 0 ? root() : getAtPath(root(), path);
319
+ if (!Object.is(selected, resolved)) {
320
+ const msg = "select(): selector must only traverse nested properties or array indexes — derived or transformed values are not supported.";
321
+ if (__DEV__) warn(msg);
322
+ throw new Error(msg);
323
+ }
324
+ if (__DEV__ && path.length === 0) warn("select(): selector did not traverse nested state — bind writes may replace the entire root value.");
325
+ return markSignalAccessor((...args) => {
326
+ if (args.length === 0) return path.length === 0 ? selector(root()) : getAtPath(root(), path);
327
+ const next = path.length === 0 ? args[0] : setAtPath(root(), path, args[0]);
328
+ if (!Object.is(root(), next)) root(next);
329
+ });
178
330
  }
179
331
  function ilhaRaw(value) {
180
332
  return {
@@ -229,6 +381,19 @@ function normalizeJsxChildren(props, children) {
229
381
  const propChildren = props && "children" in props ? props.children : void 0;
230
382
  return (children.length > 0 ? children : propChildren === void 0 ? [] : [propChildren]).flat(1);
231
383
  }
384
+ function normalizeJsxSlotKey(raw) {
385
+ const key = String(raw);
386
+ if (key.trim().length === 0) throw new Error("jsx key requires a non-empty string.");
387
+ if (key.includes(":")) throw new Error(`jsx key cannot contain the slot separator ":" (got "${key}").`);
388
+ return key;
389
+ }
390
+ /** React-style `key` (props or 3rd jsx arg) on island components → `k:{key}` slot ids. */
391
+ function extractJsxSlotKey(props, keyArg) {
392
+ const fromProps = props?.key;
393
+ const raw = keyArg ?? (typeof fromProps === "string" || typeof fromProps === "number" ? fromProps : void 0);
394
+ if (raw == null) return void 0;
395
+ return normalizeJsxSlotKey(raw);
396
+ }
232
397
  function serializeStyle(value) {
233
398
  return Object.entries(value).map(([k, v]) => {
234
399
  if (!SAFE_CSS_PROP_RE.test(k)) return "";
@@ -281,17 +446,25 @@ function renderJsxElement(type, props, children) {
281
446
  } else if (children.length > 0 && __DEV__) warn(`Void element <${type}> should not have children. They will be ignored.`);
282
447
  return ilhaHtml(chunks, ...values);
283
448
  }
284
- function ilhaJsx(type, props, ...children) {
285
- const normalizedChildren = normalizeJsxChildren(props, children);
449
+ function ilhaJsx(type, props, maybeKey, ...restChildren) {
450
+ const hasKeyArg = typeof maybeKey === "string" || typeof maybeKey === "number";
451
+ const keyFromArg = hasKeyArg ? maybeKey : void 0;
452
+ const normalizedChildren = normalizeJsxChildren(props, hasKeyArg || maybeKey === void 0 ? restChildren : [maybeKey, ...restChildren]);
286
453
  if (typeof type === "function") {
454
+ const slotKey = extractJsxSlotKey(props, typeof keyFromArg === "string" || typeof keyFromArg === "number" ? keyFromArg : void 0);
287
455
  const componentProps = {
288
456
  ...props ?? {},
289
457
  ...normalizedChildren.length > 0 ? { children: normalizedChildren } : {}
290
458
  };
291
459
  delete componentProps["key"];
292
460
  const out = type(Object.keys(componentProps).length ? componentProps : {});
461
+ if (isIslandCall(out)) {
462
+ if (slotKey !== void 0) out.key = slotKey;
463
+ return ilhaHtml`${out}`;
464
+ }
465
+ if (isRawHtml(out)) return out;
466
+ if (typeof out === "string" && isIsland(type)) return ilhaRaw(emitIslandSlot(type, componentProps, slotKey));
293
467
  if (typeof out === "string") return ilhaHtml`${out}`;
294
- if (out && typeof out === "object" && RAW in out) return out;
295
468
  return ilhaHtml`${out}`;
296
469
  }
297
470
  return renderJsxElement(type, props, normalizedChildren);
@@ -300,13 +473,13 @@ function ilhaFragment(props, ...children) {
300
473
  const normalizedChildren = normalizeJsxChildren(props, children);
301
474
  return ilhaHtml(["", ...normalizedChildren.map(() => "")], ...normalizedChildren);
302
475
  }
303
- function ilhaJsxDEV(type, props) {
304
- return ilhaJsx(type, props);
476
+ function ilhaJsxDEV(type, props, maybeKey, _source, _self) {
477
+ return ilhaJsx(type, props, maybeKey);
305
478
  }
306
479
  function interpolateValue(v) {
307
480
  if (v == null || v === true || v === false) return "";
308
481
  if (Array.isArray(v)) return v.map(interpolateValue).join("");
309
- if (typeof v === "object" && RAW in v) return v.value;
482
+ if (isRawHtml(v)) return v.value;
310
483
  if (isIslandCall(v)) {
311
484
  const call = typeof v === "function" ? v() : v;
312
485
  return emitIslandSlot(call.island, call.props, call.key);
@@ -432,8 +605,15 @@ function ilhaHtml(strings, ...values) {
432
605
  value: dedentString(result)
433
606
  };
434
607
  }
608
+ /** Duck-type RawHtml — works across duplicate `ilha` bundles (distinct RAW symbols). */
609
+ function isRawHtml(v) {
610
+ if (typeof v !== "object" || v === null || !("value" in v)) return false;
611
+ if (typeof v.value !== "string") return false;
612
+ if (RAW in v) return true;
613
+ return Object.getOwnPropertySymbols(v).some((s) => s.description === "ilha.raw");
614
+ }
435
615
  function unwrapHtml(v) {
436
- return typeof v === "object" && RAW in v ? v.value : v;
616
+ return isRawHtml(v) ? v.value : v;
437
617
  }
438
618
  const contextRegistry = /* @__PURE__ */ new Map();
439
619
  function ilhaContext(key, initial) {
@@ -1161,10 +1341,38 @@ Element: ${host.outerHTML.slice(0, 120)}`);
1161
1341
  }
1162
1342
  }
1163
1343
  const mountedSlots = /* @__PURE__ */ new Map();
1344
+ const leavingSlots = /* @__PURE__ */ new Map();
1345
+ function teardownMountedSlot(id, entry) {
1346
+ mountedSlots.delete(id);
1347
+ const token = Symbol();
1348
+ const result = entry.unmount();
1349
+ const remove = (stub) => {
1350
+ const leaving = leavingSlots.get(id);
1351
+ if (!leaving || leaving.token !== token) return;
1352
+ leaving.el.remove();
1353
+ stub?.remove();
1354
+ leavingSlots.delete(id);
1355
+ };
1356
+ if (result instanceof Promise) {
1357
+ let stub;
1358
+ if (entry.el.isConnected) {
1359
+ stub = document.createElement("div");
1360
+ stub.setAttribute(SLOT_ATTR, id);
1361
+ entry.el.replaceWith(stub);
1362
+ }
1363
+ leavingSlots.set(id, {
1364
+ el: entry.el,
1365
+ token
1366
+ });
1367
+ result.finally(() => remove(stub));
1368
+ } else entry.el.remove();
1369
+ }
1164
1370
  function findSlot(id) {
1371
+ const leavingEls = new Set([...leavingSlots.values()].map((l) => l.el));
1165
1372
  const escaped = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(id) : id.replace(/["\\]/g, "\\$&");
1166
1373
  const candidates = host.querySelectorAll(`[${SLOT_ATTR}="${escaped}"]`);
1167
1374
  for (const candidate of candidates) {
1375
+ if (leavingEls.has(candidate)) continue;
1168
1376
  let el = candidate;
1169
1377
  while (el && el !== host) {
1170
1378
  if (el.hasAttribute("data-ilha")) break;
@@ -1175,10 +1383,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
1175
1383
  return null;
1176
1384
  }
1177
1385
  function mountSlots(slotMap) {
1178
- for (const [id, entry] of mountedSlots) if (!slotMap.has(id)) {
1179
- entry.unmount();
1180
- mountedSlots.delete(id);
1181
- }
1386
+ for (const [id, entry] of mountedSlots) if (!slotMap.has(id)) teardownMountedSlot(id, entry);
1182
1387
  for (const [id, { island: childIsland, props }] of slotMap) {
1183
1388
  const existing = mountedSlots.get(id);
1184
1389
  if (existing) {
@@ -1229,6 +1434,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
1229
1434
  listeners.splice(0, listeners.length, ...listeners.filter((l) => l.entry !== entry));
1230
1435
  }
1231
1436
  const eventTarget = event.target instanceof Element ? event.target : listenerTarget;
1437
+ const handlerTarget = entry.selector === "" ? eventTarget : listenerTarget;
1232
1438
  let handlerSignal;
1233
1439
  if (entry.abortable) {
1234
1440
  let entryMap = invocationControllers.get(entry);
@@ -1250,7 +1456,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
1250
1456
  derived,
1251
1457
  input,
1252
1458
  host,
1253
- target: eventTarget,
1459
+ target: handlerTarget,
1254
1460
  event,
1255
1461
  signal: handlerSignal
1256
1462
  });
@@ -1291,11 +1497,11 @@ Element: ${host.outerHTML.slice(0, 120)}`);
1291
1497
  input
1292
1498
  }), hasExistingContent ? host : void 0);
1293
1499
  if (!hasExistingContent) host.innerHTML = stylePrefix + initial.html;
1294
- attachListeners();
1295
1500
  let stopBindings = applyTemplateBindings(host, initial.binds);
1296
1501
  cleanups.push(() => stopBindings());
1297
1502
  mountSlots(initial.slots);
1298
1503
  cleanups.push(() => mountedSlots.forEach((entry) => entry.unmount()));
1504
+ attachListeners();
1299
1505
  if (!shouldSkipOnMount) for (const entry of onMounts) {
1300
1506
  const prevSub = setActiveSub(void 0);
1301
1507
  let userCleanup;
@@ -1365,19 +1571,34 @@ Element: ${host.outerHTML.slice(0, 120)}`);
1365
1571
  if (!initialized) {
1366
1572
  initialized = true;
1367
1573
  if (rendered === initialRenderedHtml) return;
1574
+ if (mountedSlots.size > 0 && mountedSlots.size === newSlotMap.size && [...newSlotMap.keys()].every((id) => mountedSlots.has(id)) && isStableInlineSlotMount(initialRenderedHtml, rendered, newSlotMap.keys())) return;
1368
1575
  }
1369
1576
  detachListeners();
1370
1577
  stopBindings();
1578
+ const prevMountedCount = mountedSlots.size;
1579
+ const removedSlotIds = [];
1371
1580
  for (const [id, entry] of mountedSlots) if (!newSlotMap.has(id)) {
1372
- entry.unmount();
1373
- mountedSlots.delete(id);
1581
+ teardownMountedSlot(id, entry);
1582
+ removedSlotIds.push(id);
1583
+ }
1584
+ for (const id of removedSlotIds) {
1585
+ if (leavingSlots.has(id)) continue;
1586
+ const escaped = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(id) : id.replace(/["\\]/g, "\\$&");
1587
+ for (const el of host.querySelectorAll(`[${SLOT_ATTR}="${escaped}"]`)) el.remove();
1374
1588
  }
1375
1589
  const preserved = /* @__PURE__ */ new Map();
1590
+ const listShrunk = newSlotMap.size < prevMountedCount;
1376
1591
  for (const [id, entry] of mountedSlots) {
1377
1592
  if (!newSlotMap.has(id)) continue;
1378
1593
  if (!entry.el.isConnected) continue;
1594
+ if (listShrunk && id.startsWith("p:")) {
1595
+ teardownMountedSlot(id, entry);
1596
+ continue;
1597
+ }
1379
1598
  const stub = document.createElement("div");
1380
1599
  stub.setAttribute(SLOT_ATTR, id);
1600
+ const incomingProps = serializeSlotProps(newSlotMap.get(id)?.props);
1601
+ if (incomingProps !== "") stub.setAttribute(PROPS_ATTR, incomingProps);
1381
1602
  entry.el.replaceWith(stub);
1382
1603
  preserved.set(id, entry.el);
1383
1604
  }
@@ -1400,16 +1621,23 @@ Element: ${host.outerHTML.slice(0, 120)}`);
1400
1621
  if (__DEV__ && _mountedHosts) _mountedHosts.delete(host);
1401
1622
  stopRender();
1402
1623
  detachListeners();
1624
+ const pending = [];
1625
+ for (const [, entry] of mountedSlots) {
1626
+ const result = entry.unmount();
1627
+ if (result instanceof Promise) pending.push(result);
1628
+ }
1403
1629
  if (transition?.leave) {
1404
1630
  const result = transition.leave(host);
1405
- if (result instanceof Promise) {
1406
- result.then(() => {
1407
- for (const c of cleanups) c();
1408
- }).catch(console.error);
1409
- return;
1410
- }
1631
+ if (result instanceof Promise) pending.push(result);
1411
1632
  }
1412
- for (const c of cleanups) c();
1633
+ const finish = () => {
1634
+ for (const c of cleanups) c();
1635
+ };
1636
+ if (pending.length > 0) return Promise.all(pending).then(finish).catch((err) => {
1637
+ console.error(err);
1638
+ finish();
1639
+ });
1640
+ finish();
1413
1641
  };
1414
1642
  const updateProps = (nextProps) => {
1415
1643
  if (tornDown) return;
@@ -1569,7 +1797,14 @@ function mountAll(registry, options = {}) {
1569
1797
  io.disconnect();
1570
1798
  });
1571
1799
  } else els.forEach(activateEl);
1572
- return { unmount: () => unmounts.forEach((u) => u()) };
1800
+ return { unmount: () => {
1801
+ const pending = [];
1802
+ for (const u of unmounts) {
1803
+ const result = u();
1804
+ if (result instanceof Promise) pending.push(result);
1805
+ }
1806
+ if (pending.length > 0) return Promise.all(pending).then(() => {});
1807
+ } };
1573
1808
  }
1574
1809
  const rootBuilder = new IlhaBuilder({
1575
1810
  schema: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilha",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "A tiny, framework-free island architecture library",
5
5
  "license": "MIT",
6
6
  "author": "Ryuz <ryuzer@proton.me>",