ilha 0.5.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,10 +51,19 @@ 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;
57
58
  declare function ilhaCss(strings: TemplateStringsArray | string, ...values: (string | number)[]): string;
59
+ type JsxChild = unknown;
60
+ type JsxProps = Record<string, unknown> | null | undefined;
61
+ type JsxType = string | ((props: Record<string, unknown>) => unknown);
62
+ declare function ilhaJsx(type: JsxType, props: JsxProps, maybeKey?: JsxChild | string | number, ...restChildren: JsxChild[]): RawHtml;
63
+ declare function ilhaFragment(props: {
64
+ children?: JsxChild;
65
+ } | null, ...children: JsxChild[]): RawHtml;
66
+ declare function ilhaJsxDEV(type: JsxType, props: JsxProps, maybeKey?: string | number, _source?: unknown, _self?: unknown): RawHtml;
58
67
  declare function ilhaHtml(strings: TemplateStringsArray, ...values: unknown[]): RawHtml;
59
68
  type ContextSignal<T> = {
60
69
  (): T;
@@ -72,7 +81,7 @@ declare function ilhaContext<T>(key: string, initial: T): ContextSignal<T>;
72
81
  * or `.render()` automatically subscribes the surrounding reactive scope —
73
82
  * so when the signal changes, dependents re-run as if it were local state.
74
83
  */
75
- declare function ilhaSignal<T>(initial: T): ExternalSignal<T>;
84
+ declare function ilhaSignal<T>(initial: T): SignalAccessor<T>;
76
85
  /**
77
86
  * Run `fn` with reactive tracking suspended. Reading signals inside `fn`
78
87
  * returns their current value without subscribing the surrounding scope.
@@ -107,10 +116,7 @@ interface DerivedEntry<TInput, TStateMap extends Record<string, unknown>> {
107
116
  fn: DerivedFn<TInput, TStateMap, unknown>;
108
117
  }
109
118
  type IslandDerived<TDerivedMap extends Record<string, unknown>> = { readonly [K in keyof TDerivedMap]: DerivedValue<TDerivedMap[K]> };
110
- interface ExternalSignal<T = unknown> {
111
- (): T;
112
- (value: T): void;
113
- }
119
+ type ExternalSignal<T = unknown> = SignalAccessor<T>;
114
120
  type SignalAccessor<T> = MarkedSignalAccessor<T>;
115
121
  type MergeState<TStateMap extends Record<string, unknown>, K extends string, V> = Omit<TStateMap, K> & Record<K, V>;
116
122
  type IslandState<TStateMap extends Record<string, unknown>> = { readonly [K in keyof TStateMap]-?: SignalAccessor<TStateMap[K]> };
@@ -235,7 +241,7 @@ interface MountOptions {
235
241
  lazy?: boolean;
236
242
  }
237
243
  interface MountResult {
238
- unmount: () => void;
244
+ unmount: () => void | Promise<void>;
239
245
  }
240
246
  interface BuilderConfig<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>> {
241
247
  schema: StandardSchemaV1 | null;
@@ -269,6 +275,10 @@ declare function mountAll(registry: IslandRegistry, options?: MountOptions): Mou
269
275
  declare const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
270
276
  html: typeof ilhaHtml;
271
277
  raw: typeof ilhaRaw;
278
+ jsx: typeof ilhaJsx;
279
+ jsxs: typeof ilhaJsx;
280
+ jsxDEV: typeof ilhaJsxDEV;
281
+ Fragment: typeof ilhaFragment;
272
282
  mount: typeof mountAll;
273
283
  from: typeof ilhaFrom;
274
284
  context: typeof ilhaContext;
@@ -278,9 +288,13 @@ declare const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, R
278
288
  };
279
289
  declare const html: typeof ilhaHtml;
280
290
  declare const raw: typeof ilhaRaw;
291
+ declare const jsx: typeof ilhaJsx;
292
+ declare const jsxs: typeof ilhaJsx;
293
+ declare const jsxDEV: typeof ilhaJsxDEV;
294
+ declare const Fragment: typeof ilhaFragment;
281
295
  declare const css: typeof ilhaCss;
282
296
  declare const mount: typeof mountAll;
283
297
  declare const from: typeof ilhaFrom;
284
298
  declare const context: typeof ilhaContext;
285
299
  //#endregion
286
- export { DerivedValue, ErrorContext, ErrorSource, ExternalSignal, HandlerContext, HandlerContextFor, HydratableOptions, ISLAND_MOUNT_INTERNAL, Island, IslandDerived, IslandState, KeyedIsland, MountOptions, MountResult, OnMountContext, RawHtml, SignalAccessor, batch, context, css, ilha as default, from, html, ilhaSignal, ilhaSignal as signal, mount, raw, untrack };
300
+ export { DerivedValue, ErrorContext, ErrorSource, ExternalSignal, Fragment, HandlerContext, HandlerContextFor, HydratableOptions, ISLAND_MOUNT_INTERNAL, Island, IslandDerived, IslandState, KeyedIsland, MountOptions, MountResult, OnMountContext, RawHtml, SignalAccessor, batch, context, css, ilha as default, from, html, ilhaSignal, ilhaSignal as signal, jsx, jsxDEV, jsxs, mount, raw, untrack };
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 {
@@ -191,10 +343,143 @@ function ilhaCss(strings, ...values) {
191
343
  }
192
344
  return result;
193
345
  }
346
+ const SAFE_NAME_RE = /^[A-Za-z_:][A-Za-z0-9:._-]*$/;
347
+ const SAFE_BIND_LOCAL_RE = /^[A-Za-z][A-Za-z0-9]*$/;
348
+ const VOID_ELEMENTS = new Set([
349
+ "area",
350
+ "base",
351
+ "br",
352
+ "col",
353
+ "embed",
354
+ "hr",
355
+ "img",
356
+ "input",
357
+ "link",
358
+ "meta",
359
+ "param",
360
+ "source",
361
+ "track",
362
+ "wbr"
363
+ ]);
364
+ const SAFE_CSS_PROP_RE = /^(-{2}[a-zA-Z][a-zA-Z0-9-]*|-?[a-zA-Z][a-zA-Z0-9-]*)$/;
365
+ const URL_ATTRS = new Set([
366
+ "href",
367
+ "src",
368
+ "action",
369
+ "formaction",
370
+ "cite",
371
+ "data",
372
+ "poster"
373
+ ]);
374
+ const SAFE_URL_RE = /^(?!javascript:|data:text\/html|data:text\/xml|data:application\/xhtml\+xml|data:image\/svg|vbscript:)/i;
375
+ function normalizeClass(value) {
376
+ if (Array.isArray(value)) return value.filter(Boolean).join(" ");
377
+ if (value && typeof value === "object") return Object.entries(value).filter(([, enabled]) => Boolean(enabled)).map(([name]) => name).join(" ");
378
+ return String(value);
379
+ }
380
+ function normalizeJsxChildren(props, children) {
381
+ const propChildren = props && "children" in props ? props.children : void 0;
382
+ return (children.length > 0 ? children : propChildren === void 0 ? [] : [propChildren]).flat(1);
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
+ }
397
+ function serializeStyle(value) {
398
+ return Object.entries(value).map(([k, v]) => {
399
+ if (!SAFE_CSS_PROP_RE.test(k)) return "";
400
+ const prop = k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
401
+ let safeV = String(v).replace(/["<>{};]/g, "");
402
+ if (/expression\(/i.test(safeV) || /^javascript:/i.test(safeV)) return "";
403
+ return `${prop}:${safeV}`;
404
+ }).filter(Boolean).join(";");
405
+ }
406
+ function pushJsxAttr(chunks, values, name, value) {
407
+ if (value == null || value === false || name === "children" || name === "key") return;
408
+ if (name === "__proto__" || name === "constructor" || name === "prototype") return;
409
+ if (name.startsWith("bind:")) {
410
+ const [prefix, localName, ...rest] = name.split(":");
411
+ if (prefix !== "bind" || rest.length > 0 || !localName || !SAFE_BIND_LOCAL_RE.test(localName)) return;
412
+ if (!isSignalAccessor(value)) return;
413
+ const safeName = `${prefix}:${localName}`;
414
+ chunks[chunks.length - 1] += ` ${safeName}=`;
415
+ values.push(value);
416
+ chunks.push("");
417
+ return;
418
+ }
419
+ if (!SAFE_NAME_RE.test(name)) return;
420
+ let safeName = name;
421
+ if (safeName === "className") safeName = "class";
422
+ if (safeName === "htmlFor") safeName = "for";
423
+ if (safeName.startsWith("on")) return;
424
+ if (safeName === "class") value = normalizeClass(value);
425
+ if (safeName === "style" && value && typeof value === "object") value = serializeStyle(value);
426
+ if ((URL_ATTRS.has(safeName) || /:(href|src|action|formaction|cite|data|poster)$/.test(safeName)) && typeof value === "string" && !SAFE_URL_RE.test(value.trim())) return;
427
+ if (value === true) {
428
+ chunks[chunks.length - 1] += ` ${safeName}`;
429
+ return;
430
+ }
431
+ chunks[chunks.length - 1] += ` ${safeName}="`;
432
+ values.push(value);
433
+ chunks.push("\"");
434
+ }
435
+ function renderJsxElement(type, props, children) {
436
+ const chunks = [`<${type}`];
437
+ const values = [];
438
+ if (props) for (const [name, value] of Object.entries(props)) pushJsxAttr(chunks, values, name, value);
439
+ chunks[chunks.length - 1] += ">";
440
+ if (!VOID_ELEMENTS.has(type)) {
441
+ for (const child of children) {
442
+ values.push(child);
443
+ chunks.push("");
444
+ }
445
+ chunks[chunks.length - 1] += `</${type}>`;
446
+ } else if (children.length > 0 && __DEV__) warn(`Void element <${type}> should not have children. They will be ignored.`);
447
+ return ilhaHtml(chunks, ...values);
448
+ }
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]);
453
+ if (typeof type === "function") {
454
+ const slotKey = extractJsxSlotKey(props, typeof keyFromArg === "string" || typeof keyFromArg === "number" ? keyFromArg : void 0);
455
+ const componentProps = {
456
+ ...props ?? {},
457
+ ...normalizedChildren.length > 0 ? { children: normalizedChildren } : {}
458
+ };
459
+ delete componentProps["key"];
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));
467
+ if (typeof out === "string") return ilhaHtml`${out}`;
468
+ return ilhaHtml`${out}`;
469
+ }
470
+ return renderJsxElement(type, props, normalizedChildren);
471
+ }
472
+ function ilhaFragment(props, ...children) {
473
+ const normalizedChildren = normalizeJsxChildren(props, children);
474
+ return ilhaHtml(["", ...normalizedChildren.map(() => "")], ...normalizedChildren);
475
+ }
476
+ function ilhaJsxDEV(type, props, maybeKey, _source, _self) {
477
+ return ilhaJsx(type, props, maybeKey);
478
+ }
194
479
  function interpolateValue(v) {
195
- if (v == null) return "";
480
+ if (v == null || v === true || v === false) return "";
196
481
  if (Array.isArray(v)) return v.map(interpolateValue).join("");
197
- if (typeof v === "object" && RAW in v) return v.value;
482
+ if (isRawHtml(v)) return v.value;
198
483
  if (isIslandCall(v)) {
199
484
  const call = typeof v === "function" ? v() : v;
200
485
  return emitIslandSlot(call.island, call.props, call.key);
@@ -216,7 +501,11 @@ const BIND_VALID_KINDS = new Set([
216
501
  ]);
217
502
  const BIND_PREFIX_RE = /\bbind:([a-zA-Z]+)\s*=\s*("|')?$/;
218
503
  function formatDateForInput(d) {
219
- if (d instanceof Date && !isNaN(d.getTime())) {
504
+ if (d instanceof Date) {
505
+ if (isNaN(d.getTime())) {
506
+ if (__DEV__) warn("bind:valueAsDate received an invalid Date object — value attribute will be empty.");
507
+ return "";
508
+ }
220
509
  const pad = (n) => String(n).padStart(2, "0");
221
510
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
222
511
  }
@@ -316,8 +605,15 @@ function ilhaHtml(strings, ...values) {
316
605
  value: dedentString(result)
317
606
  };
318
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
+ }
319
615
  function unwrapHtml(v) {
320
- return typeof v === "object" && RAW in v ? v.value : v;
616
+ return isRawHtml(v) ? v.value : v;
321
617
  }
322
618
  const contextRegistry = /* @__PURE__ */ new Map();
323
619
  function ilhaContext(key, initial) {
@@ -1045,10 +1341,38 @@ Element: ${host.outerHTML.slice(0, 120)}`);
1045
1341
  }
1046
1342
  }
1047
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
+ }
1048
1370
  function findSlot(id) {
1371
+ const leavingEls = new Set([...leavingSlots.values()].map((l) => l.el));
1049
1372
  const escaped = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(id) : id.replace(/["\\]/g, "\\$&");
1050
1373
  const candidates = host.querySelectorAll(`[${SLOT_ATTR}="${escaped}"]`);
1051
1374
  for (const candidate of candidates) {
1375
+ if (leavingEls.has(candidate)) continue;
1052
1376
  let el = candidate;
1053
1377
  while (el && el !== host) {
1054
1378
  if (el.hasAttribute("data-ilha")) break;
@@ -1059,10 +1383,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
1059
1383
  return null;
1060
1384
  }
1061
1385
  function mountSlots(slotMap) {
1062
- for (const [id, entry] of mountedSlots) if (!slotMap.has(id)) {
1063
- entry.unmount();
1064
- mountedSlots.delete(id);
1065
- }
1386
+ for (const [id, entry] of mountedSlots) if (!slotMap.has(id)) teardownMountedSlot(id, entry);
1066
1387
  for (const [id, { island: childIsland, props }] of slotMap) {
1067
1388
  const existing = mountedSlots.get(id);
1068
1389
  if (existing) {
@@ -1113,6 +1434,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
1113
1434
  listeners.splice(0, listeners.length, ...listeners.filter((l) => l.entry !== entry));
1114
1435
  }
1115
1436
  const eventTarget = event.target instanceof Element ? event.target : listenerTarget;
1437
+ const handlerTarget = entry.selector === "" ? eventTarget : listenerTarget;
1116
1438
  let handlerSignal;
1117
1439
  if (entry.abortable) {
1118
1440
  let entryMap = invocationControllers.get(entry);
@@ -1134,7 +1456,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
1134
1456
  derived,
1135
1457
  input,
1136
1458
  host,
1137
- target: eventTarget,
1459
+ target: handlerTarget,
1138
1460
  event,
1139
1461
  signal: handlerSignal
1140
1462
  });
@@ -1175,11 +1497,11 @@ Element: ${host.outerHTML.slice(0, 120)}`);
1175
1497
  input
1176
1498
  }), hasExistingContent ? host : void 0);
1177
1499
  if (!hasExistingContent) host.innerHTML = stylePrefix + initial.html;
1178
- attachListeners();
1179
1500
  let stopBindings = applyTemplateBindings(host, initial.binds);
1180
1501
  cleanups.push(() => stopBindings());
1181
1502
  mountSlots(initial.slots);
1182
1503
  cleanups.push(() => mountedSlots.forEach((entry) => entry.unmount()));
1504
+ attachListeners();
1183
1505
  if (!shouldSkipOnMount) for (const entry of onMounts) {
1184
1506
  const prevSub = setActiveSub(void 0);
1185
1507
  let userCleanup;
@@ -1249,19 +1571,34 @@ Element: ${host.outerHTML.slice(0, 120)}`);
1249
1571
  if (!initialized) {
1250
1572
  initialized = true;
1251
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;
1252
1575
  }
1253
1576
  detachListeners();
1254
1577
  stopBindings();
1578
+ const prevMountedCount = mountedSlots.size;
1579
+ const removedSlotIds = [];
1255
1580
  for (const [id, entry] of mountedSlots) if (!newSlotMap.has(id)) {
1256
- entry.unmount();
1257
- 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();
1258
1588
  }
1259
1589
  const preserved = /* @__PURE__ */ new Map();
1590
+ const listShrunk = newSlotMap.size < prevMountedCount;
1260
1591
  for (const [id, entry] of mountedSlots) {
1261
1592
  if (!newSlotMap.has(id)) continue;
1262
1593
  if (!entry.el.isConnected) continue;
1594
+ if (listShrunk && id.startsWith("p:")) {
1595
+ teardownMountedSlot(id, entry);
1596
+ continue;
1597
+ }
1263
1598
  const stub = document.createElement("div");
1264
1599
  stub.setAttribute(SLOT_ATTR, id);
1600
+ const incomingProps = serializeSlotProps(newSlotMap.get(id)?.props);
1601
+ if (incomingProps !== "") stub.setAttribute(PROPS_ATTR, incomingProps);
1265
1602
  entry.el.replaceWith(stub);
1266
1603
  preserved.set(id, entry.el);
1267
1604
  }
@@ -1284,16 +1621,23 @@ Element: ${host.outerHTML.slice(0, 120)}`);
1284
1621
  if (__DEV__ && _mountedHosts) _mountedHosts.delete(host);
1285
1622
  stopRender();
1286
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
+ }
1287
1629
  if (transition?.leave) {
1288
1630
  const result = transition.leave(host);
1289
- if (result instanceof Promise) {
1290
- result.then(() => {
1291
- for (const c of cleanups) c();
1292
- }).catch(console.error);
1293
- return;
1294
- }
1631
+ if (result instanceof Promise) pending.push(result);
1295
1632
  }
1296
- 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();
1297
1641
  };
1298
1642
  const updateProps = (nextProps) => {
1299
1643
  if (tornDown) return;
@@ -1453,7 +1797,14 @@ function mountAll(registry, options = {}) {
1453
1797
  io.disconnect();
1454
1798
  });
1455
1799
  } else els.forEach(activateEl);
1456
- 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
+ } };
1457
1808
  }
1458
1809
  const rootBuilder = new IlhaBuilder({
1459
1810
  schema: null,
@@ -1469,6 +1820,10 @@ const rootBuilder = new IlhaBuilder({
1469
1820
  const ilha = Object.assign(rootBuilder, {
1470
1821
  html: ilhaHtml,
1471
1822
  raw: ilhaRaw,
1823
+ jsx: ilhaJsx,
1824
+ jsxs: ilhaJsx,
1825
+ jsxDEV: ilhaJsxDEV,
1826
+ Fragment: ilhaFragment,
1472
1827
  mount: mountAll,
1473
1828
  from: ilhaFrom,
1474
1829
  context: ilhaContext,
@@ -1478,9 +1833,13 @@ const ilha = Object.assign(rootBuilder, {
1478
1833
  });
1479
1834
  const html = ilhaHtml;
1480
1835
  const raw = ilhaRaw;
1836
+ const jsx = ilhaJsx;
1837
+ const jsxs = ilhaJsx;
1838
+ const jsxDEV = ilhaJsxDEV;
1839
+ const Fragment = ilhaFragment;
1481
1840
  const css = ilhaCss;
1482
1841
  const mount = mountAll;
1483
1842
  const from = ilhaFrom;
1484
1843
  const context = ilhaContext;
1485
1844
  //#endregion
1486
- export { ISLAND_MOUNT_INTERNAL, batch, context, css, ilha as default, from, html, ilhaSignal, ilhaSignal as signal, mount, raw, untrack };
1845
+ export { Fragment, ISLAND_MOUNT_INTERNAL, batch, context, css, ilha as default, from, html, ilhaSignal, ilhaSignal as signal, jsx, jsxDEV, jsxs, mount, raw, untrack };
@@ -0,0 +1,3 @@
1
+ import { Fragment, jsxDEV } from "./index.js";
2
+ import { t as JSX } from "./jsx-types-DSrM2NdI.js";
3
+ export { Fragment, type JSX, jsxDEV };
@@ -0,0 +1,2 @@
1
+ import { Fragment, jsxDEV } from "./index.js";
2
+ export { Fragment, jsxDEV };
@@ -0,0 +1,3 @@
1
+ import { Fragment, jsx, jsxs } from "./index.js";
2
+ import { t as JSX } from "./jsx-types-DSrM2NdI.js";
3
+ export { Fragment, type JSX, jsx, jsxs };
@@ -0,0 +1,2 @@
1
+ import { Fragment, jsx, jsxs } from "./index.js";
2
+ export { Fragment, jsx, jsxs };
@@ -0,0 +1,26 @@
1
+ import { RawHtml } from "./index.js";
2
+
3
+ //#region src/jsx-types.d.ts
4
+ declare namespace JSX {
5
+ type Child = unknown;
6
+ type ElementType = string | ((props: any) => any);
7
+ interface Element extends RawHtml {}
8
+ interface ElementChildrenAttribute {
9
+ children: {};
10
+ }
11
+ interface IntrinsicAttributes {
12
+ key?: string | number;
13
+ }
14
+ type IntrinsicElementProps = {
15
+ children?: Child;
16
+ class?: string | unknown[] | Record<string, boolean>;
17
+ className?: string | unknown[] | Record<string, boolean>;
18
+ htmlFor?: string;
19
+ [name: string]: unknown;
20
+ };
21
+ interface IntrinsicElements {
22
+ [name: string]: IntrinsicElementProps;
23
+ }
24
+ }
25
+ //#endregion
26
+ export { JSX as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilha",
3
- "version": "0.5.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>",
@@ -19,6 +19,14 @@
19
19
  ".": {
20
20
  "types": "./dist/index.d.ts",
21
21
  "import": "./dist/index.js"
22
+ },
23
+ "./jsx-runtime": {
24
+ "types": "./dist/jsx-runtime.d.ts",
25
+ "import": "./dist/jsx-runtime.js"
26
+ },
27
+ "./jsx-dev-runtime": {
28
+ "types": "./dist/jsx-dev-runtime.d.ts",
29
+ "import": "./dist/jsx-dev-runtime.js"
22
30
  }
23
31
  },
24
32
  "scripts": {