jsswift 1.0.51 → 1.0.53

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/jsswift.js CHANGED
@@ -661,8 +661,7 @@
661
661
  const schedulePosition = () => {
662
662
  if (!opts.anchorEl) return;
663
663
  const raf = globalThis.requestAnimationFrame || ((fn) => setTimeout(() => fn(Date.now()), 0));
664
- const caf = globalThis.cancelAnimationFrame || clearTimeout;
665
- if (entry._positionFrame != null) caf(entry._positionFrame);
664
+ if (entry._positionFrame != null) return;
666
665
  entry._positionFrame = raf(() => {
667
666
  entry._positionFrame = null;
668
667
  position();
@@ -673,13 +672,13 @@
673
672
  if (opts.anchorEl) {
674
673
  position();
675
674
  schedulePosition();
676
- const onResize = () => position();
675
+ const onResize = schedulePosition;
677
676
  window.addEventListener("resize", onResize);
678
677
  window.addEventListener("scroll", onResize, true);
679
678
  window.visualViewport?.addEventListener?.("resize", onResize);
680
679
  window.visualViewport?.addEventListener?.("scroll", onResize);
681
680
  if (typeof ResizeObserver === "function") {
682
- const panelResizeObserver = new ResizeObserver(() => position());
681
+ const panelResizeObserver = new ResizeObserver(schedulePosition);
683
682
  panelResizeObserver.observe(panel);
684
683
  entry._panelResizeObserver = panelResizeObserver;
685
684
  }
@@ -2340,163 +2339,175 @@ _.dynamic = function (renderFn) {
2340
2339
 
2341
2340
  return r;
2342
2341
  };
2343
- // ===============================
2344
- // Two-way binding: input <-> rod
2345
- // ===============================
2346
- // ===============================
2347
- // Two-way binding avanzato
2348
- // input / checkbox / radio / select
2349
- // ===============================
2350
-
2351
- function getInputKind(el) {
2352
- if (!el || !el.tagName) return null;
2353
-
2354
- const tag = el.tagName.toLowerCase();
2355
-
2356
- if (tag === "input") {
2357
- const type = (el.type || "text").toLowerCase();
2358
- if (type === "checkbox") return "checkbox";
2359
- if (type === "radio") return "radio";
2360
- if (type === "number") return "number";
2361
- return "text";
2362
- }
2363
-
2364
- if (tag === "select") {
2365
- return el.multiple ? "select-multiple" : "select";
2366
- }
2367
-
2368
- return null;
2369
- }
2370
-
2371
- JSswift.rodModel = function (target, rodObj, opts = {}) {
2372
- if (!rodObj || rodObj.type !== "rod") {
2373
- throw new Error("[JSswift.rodModel] rodObj deve essere un rod");
2374
- }
2375
-
2376
- // Supporta singolo elemento o lista (radio)
2377
- const elements = Array.isArray(target) || target instanceof NodeList
2378
- ? Array.from(target)
2379
- : [target];
2380
-
2381
- const eventName = opts.event ?? "change";
2382
- const parse = opts.parse ?? (v => v);
2383
- const format = opts.format ?? (v => v);
2384
-
2385
- let syncing = false;
2386
- const disposers = [];
2387
-
2388
- // === ROD → DOM ===
2389
- const updateDOM = (value) => {
2390
- for (const el of elements) {
2391
- if (!el) continue;
2392
- const kind = getInputKind(el);
2393
-
2394
- switch (kind) {
2395
- case "checkbox":
2396
- el.checked = !!value;
2397
- break;
2398
-
2399
- case "radio":
2400
- el.checked = el.value == value;
2401
- break;
2402
-
2403
- case "select":
2404
- el.value = value ?? "";
2405
- break;
2406
-
2407
- case "select-multiple":
2408
- const arr = Array.isArray(value) ? value.map(String) : [];
2409
- for (const opt of el.options) {
2410
- opt.selected = arr.includes(opt.value);
2411
- }
2412
- break;
2413
-
2414
- case "number":
2415
- case "text":
2416
- default:
2417
- el.value = value ?? "";
2418
- }
2419
- }
2420
- };
2421
-
2422
- // bind rod → DOM (usa rod.action)
2423
- rodObj.action((v) => {
2424
- if (syncing) return;
2425
- syncing = true;
2426
- try {
2427
- updateDOM(format(v));
2428
- } finally {
2429
- syncing = false;
2430
- }
2431
- });
2432
-
2433
- // sync iniziale
2434
- updateDOM(format(rodObj.value));
2435
-
2436
- // === DOM → ROD ===
2437
- for (const el of elements) {
2438
- if (!el) continue;
2439
-
2440
- const kind = getInputKind(el);
2441
-
2442
- const handler = () => {
2443
- if (syncing) return;
2444
-
2445
- let next;
2446
-
2447
- switch (kind) {
2448
- case "checkbox":
2449
- next = el.checked;
2450
- break;
2451
-
2452
- case "radio":
2453
- if (!el.checked) return;
2454
- next = el.value;
2455
- break;
2456
-
2457
- case "select":
2458
- next = el.value;
2459
- break;
2460
-
2461
- case "select-multiple":
2462
- next = Array.from(el.selectedOptions).map(o => o.value);
2463
- break;
2464
-
2465
- case "number":
2466
- next = parse(el.value === "" ? null : Number(el.value));
2467
- break;
2468
-
2469
- case "text":
2470
- default:
2471
- next = parse(el.value);
2472
- }
2473
-
2474
- syncing = true;
2475
- try {
2476
- if (rodObj.value !== next) rodObj.value = next;
2477
- } finally {
2478
- syncing = false;
2479
- }
2480
- };
2481
-
2482
- el.addEventListener(eventName, handler);
2483
- disposers.push(() => el.removeEventListener(eventName, handler));
2484
- }
2485
-
2486
- // cleanup automatico
2487
- if (typeof rodObj.onDispose === "function") {
2488
- rodObj.onDispose(() => disposers.forEach(fn => fn()));
2489
- }
2490
-
2491
- return () => disposers.forEach(fn => fn());
2492
- };
2493
-
2494
-
2495
- // input <-> signal (sugar)
2496
- JSswift.signalModel = function (inputEl, get, set, opts = {}) {
2497
- const r = JSswift.rodFromSignal(get, set);
2498
- return JSswift.rodModel(inputEl, r, opts);
2499
- };
2342
+ // ===============================
2343
+ // Two-way binding: input <-> rod
2344
+ // ===============================
2345
+ // ===============================
2346
+ // Two-way binding avanzato
2347
+ // input / checkbox / radio / select
2348
+ // ===============================
2349
+
2350
+ function getInputKind(el) {
2351
+ if (!el || !el.tagName) return null;
2352
+
2353
+ const tag = el.tagName.toLowerCase();
2354
+
2355
+ if (tag === "input") {
2356
+ const type = (el.type || "text").toLowerCase();
2357
+ if (type === "checkbox") return "checkbox";
2358
+ if (type === "radio") return "radio";
2359
+ if (type === "number") return "number";
2360
+ return "text";
2361
+ }
2362
+
2363
+ if (tag === "select") {
2364
+ return el.multiple ? "select-multiple" : "select";
2365
+ }
2366
+
2367
+ return null;
2368
+ }
2369
+
2370
+ JSswift.rodModel = function (target, rodObj, opts = {}) {
2371
+ if (!rodObj || rodObj.type !== "rod") {
2372
+ throw new Error("[JSswift.rodModel] rodObj deve essere un rod");
2373
+ }
2374
+
2375
+ // Supporta singolo elemento o lista (radio)
2376
+ const elements = Array.isArray(target) || target instanceof NodeList
2377
+ ? Array.from(target)
2378
+ : [target];
2379
+
2380
+ const eventName = opts.event ?? "change";
2381
+ const parse = opts.parse ?? (v => v);
2382
+ const format = opts.format ?? (v => v);
2383
+
2384
+ let syncing = false;
2385
+ const disposers = [];
2386
+
2387
+ // === ROD → DOM ===
2388
+ const updateDOM = (value) => {
2389
+ for (const el of elements) {
2390
+ if (!el) continue;
2391
+ const kind = getInputKind(el);
2392
+
2393
+ switch (kind) {
2394
+ case "checkbox":
2395
+ el.checked = !!value;
2396
+ break;
2397
+
2398
+ case "radio":
2399
+ el.checked = el.value == value;
2400
+ break;
2401
+
2402
+ case "select":
2403
+ el.value = value ?? "";
2404
+ break;
2405
+
2406
+ case "select-multiple":
2407
+ const arr = Array.isArray(value) ? value.map(String) : [];
2408
+ for (const opt of el.options) {
2409
+ opt.selected = arr.includes(opt.value);
2410
+ }
2411
+ break;
2412
+
2413
+ case "number":
2414
+ case "text":
2415
+ default:
2416
+ el.value = value ?? "";
2417
+ }
2418
+ }
2419
+ };
2420
+
2421
+ // bind rod → DOM (usa rod.action)
2422
+ const onValue = (v) => {
2423
+ if (syncing) return;
2424
+ syncing = true;
2425
+ try {
2426
+ updateDOM(format(v));
2427
+ } finally {
2428
+ syncing = false;
2429
+ }
2430
+ };
2431
+ rodObj.action(onValue);
2432
+
2433
+ // sync iniziale
2434
+ updateDOM(format(rodObj.value));
2435
+
2436
+ // === DOM → ROD ===
2437
+ for (const el of elements) {
2438
+ if (!el) continue;
2439
+
2440
+ const kind = getInputKind(el);
2441
+
2442
+ const handler = () => {
2443
+ if (syncing) return;
2444
+
2445
+ let next;
2446
+
2447
+ switch (kind) {
2448
+ case "checkbox":
2449
+ next = el.checked;
2450
+ break;
2451
+
2452
+ case "radio":
2453
+ if (!el.checked) return;
2454
+ next = el.value;
2455
+ break;
2456
+
2457
+ case "select":
2458
+ next = el.value;
2459
+ break;
2460
+
2461
+ case "select-multiple":
2462
+ next = Array.from(el.selectedOptions).map(o => o.value);
2463
+ break;
2464
+
2465
+ case "number":
2466
+ next = parse(el.value === "" ? null : Number(el.value));
2467
+ break;
2468
+
2469
+ case "text":
2470
+ default:
2471
+ next = parse(el.value);
2472
+ }
2473
+
2474
+ syncing = true;
2475
+ try {
2476
+ if (rodObj.value !== next) rodObj.value = next;
2477
+ } finally {
2478
+ syncing = false;
2479
+ }
2480
+ };
2481
+
2482
+ el.addEventListener(eventName, handler);
2483
+ disposers.push(() => el.removeEventListener(eventName, handler));
2484
+ }
2485
+
2486
+ // Release both directions without disposing the caller-owned rod.
2487
+ let disposed = false;
2488
+ const dispose = () => {
2489
+ if (disposed) return;
2490
+ disposed = true;
2491
+ disposers.splice(0).forEach(fn => fn());
2492
+ const actionIndex = rodObj._actions?.indexOf(onValue) ?? -1;
2493
+ if (actionIndex >= 0) rodObj._actions.splice(actionIndex, 1);
2494
+ const hookIndex = rodObj._disposeHooks?.indexOf(dispose) ?? -1;
2495
+ if (hookIndex >= 0 && !rodObj._disposed) rodObj._disposeHooks.splice(hookIndex, 1);
2496
+ };
2497
+ if (typeof rodObj.onDispose === "function") rodObj.onDispose(dispose);
2498
+ return dispose;
2499
+ };
2500
+
2501
+
2502
+ // input <-> signal (sugar)
2503
+ JSswift.signalModel = function (inputEl, get, set, opts = {}) {
2504
+ const r = JSswift.rodFromSignal(get, set);
2505
+ const stopBinding = JSswift.rodModel(inputEl, r, opts);
2506
+ return () => {
2507
+ stopBinding();
2508
+ r.dispose();
2509
+ };
2510
+ };
2500
2511
  // ===============================
2501
2512
  // Lifecycle helpers
2502
2513
  // ===============================
@@ -7731,7 +7742,8 @@ const unitCover = (v, name = 'size') => {
7731
7742
  || typeof noteSlot === "function"
7732
7743
  || typeof hintSlot === "function";
7733
7744
  if (canReact) {
7734
- JSswift.reactive.effect(() => { renderMessage(); }, "UI.FormField:message");
7745
+ const stopMessage = JSswift.reactive.effect(() => { renderMessage(); }, "UI.FormField:message");
7746
+ JSswift._registerCleanup(wrap, stopMessage);
7735
7747
  } else {
7736
7748
  renderMessage();
7737
7749
  }
@@ -7754,6 +7766,16 @@ const unitCover = (v, name = 'size') => {
7754
7766
  value: props.value
7755
7767
  });
7756
7768
 
7769
+ let autofillDisposed = false;
7770
+ const autofillTimeouts = new Set();
7771
+ const autofillLater = (fn, delay) => {
7772
+ if (autofillDisposed) return;
7773
+ const id = setTimeout(() => {
7774
+ autofillTimeouts.delete(id);
7775
+ if (!autofillDisposed) fn();
7776
+ }, delay);
7777
+ autofillTimeouts.add(id);
7778
+ };
7757
7779
  let lastKnownValue = el.value ?? "";
7758
7780
  const syncAutofill = () => {
7759
7781
  const next = el.value ?? "";
@@ -7765,7 +7787,7 @@ const unitCover = (v, name = 'size') => {
7765
7787
  let autofillTimer = null;
7766
7788
  let autofillProbe = false;
7767
7789
  const scheduleAutofillProbe = () => {
7768
- if (autofillProbe) return;
7790
+ if (autofillDisposed || autofillProbe) return;
7769
7791
  autofillProbe = true;
7770
7792
  let count = 0;
7771
7793
  const tick = () => {
@@ -7775,12 +7797,12 @@ const unitCover = (v, name = 'size') => {
7775
7797
  autofillProbe = false;
7776
7798
  return;
7777
7799
  }
7778
- setTimeout(tick, 120);
7800
+ autofillLater(tick, 120);
7779
7801
  };
7780
- setTimeout(tick, 40);
7802
+ autofillLater(tick, 40);
7781
7803
  };
7782
7804
  const startAutofillWatch = () => {
7783
- if (autofillTimer) return;
7805
+ if (autofillDisposed || autofillTimer) return;
7784
7806
  autofillTimer = setInterval(() => {
7785
7807
  if (document.activeElement !== el) {
7786
7808
  clearInterval(autofillTimer);
@@ -7806,27 +7828,33 @@ const unitCover = (v, name = 'size') => {
7806
7828
  el.addEventListener("focus", () => {
7807
7829
  startAutofillWatch();
7808
7830
  scheduleAutofillProbe();
7809
- setTimeout(syncAutofill, 50);
7831
+ autofillLater(syncAutofill, 50);
7810
7832
  });
7811
7833
  el.addEventListener("blur", () => {
7812
7834
  stopAutofillWatch();
7813
- setTimeout(syncAutofill, 0);
7835
+ autofillLater(syncAutofill, 0);
7814
7836
  });
7815
- setTimeout(syncAutofill, 0);
7837
+ autofillLater(syncAutofill, 0);
7816
7838
  scheduleAutofillProbe();
7817
7839
 
7840
+ JSswift._registerCleanup(el, () => {
7841
+ autofillDisposed = true;
7842
+ stopAutofillWatch();
7843
+ autofillTimeouts.forEach(clearTimeout);
7844
+ autofillTimeouts.clear();
7845
+ });
7846
+
7818
7847
  // model binding
7819
7848
  const model = props.model;
7820
7849
  if (model) {
7821
7850
  // Supporta rod direttamente
7822
7851
  if (typeof model === "object" && typeof model._bind === "function") {
7823
- app.rodModel(el, model);
7852
+ JSswift._registerCleanup(el, app.rodModel(el, model));
7824
7853
  }
7825
7854
  // Supporta [get,set] signal -> creiamo rodFromSignal
7826
7855
  else if (Array.isArray(model) && typeof model[0] === "function" && typeof model[1] === "function") {
7827
- const r = app.rodFromSignal(model[0], model[1]);
7828
- app.rodModel(el, r);
7829
- // NOTE: se vuoi cleanup automatico qui, lo facciamo nel layer component (v2)
7856
+ const stop = JSswift.reactive.untracked(() => app.signalModel(el, model[0], model[1]));
7857
+ JSswift._registerCleanup(el, stop);
7830
7858
  }
7831
7859
  }
7832
7860
  uiRegisterShortcode(el, props, {
@@ -7843,7 +7871,7 @@ const unitCover = (v, name = 'size') => {
7843
7871
  if (value == null || value === false) control.removeAttribute(name);
7844
7872
  else control.setAttribute(name, String(value));
7845
7873
  };
7846
- JSswift.reactive.effect(() => {
7874
+ const stopAttributes = JSswift.reactive.effect(() => {
7847
7875
  const label = uiUnwrap(props.ariaLabel) ?? (typeof props.label === "string" ? props.label : null);
7848
7876
  set("aria-label", label);
7849
7877
  set("aria-labelledby", uiUnwrap(props.ariaLabelledBy));
@@ -7858,6 +7886,7 @@ const unitCover = (v, name = 'size') => {
7858
7886
  set(name.toLowerCase(), uiUnwrap(props[name]));
7859
7887
  }
7860
7888
  }, "UI.Field:controlAttributes");
7889
+ JSswift._registerCleanup(field, stopAttributes);
7861
7890
  }
7862
7891
 
7863
7892
  UI.Input = (props = {}) => {
@@ -7877,6 +7906,16 @@ const unitCover = (v, name = 'size') => {
7877
7906
  readOnly: !!props.readonly
7878
7907
  });
7879
7908
 
7909
+ let autofillDisposed = false;
7910
+ const autofillTimeouts = new Set();
7911
+ const autofillLater = (fn, delay) => {
7912
+ if (autofillDisposed) return;
7913
+ const id = setTimeout(() => {
7914
+ autofillTimeouts.delete(id);
7915
+ if (!autofillDisposed) fn();
7916
+ }, delay);
7917
+ autofillTimeouts.add(id);
7918
+ };
7880
7919
  let lastKnownValue = input.value ?? "";
7881
7920
  const syncAutofill = () => {
7882
7921
  const next = input.value ?? "";
@@ -7888,7 +7927,7 @@ const unitCover = (v, name = 'size') => {
7888
7927
  let autofillTimer = null;
7889
7928
  let autofillProbe = false;
7890
7929
  const scheduleAutofillProbe = () => {
7891
- if (autofillProbe) return;
7930
+ if (autofillDisposed || autofillProbe) return;
7892
7931
  autofillProbe = true;
7893
7932
  let count = 0;
7894
7933
  const tick = () => {
@@ -7898,12 +7937,12 @@ const unitCover = (v, name = 'size') => {
7898
7937
  autofillProbe = false;
7899
7938
  return;
7900
7939
  }
7901
- setTimeout(tick, 120);
7940
+ autofillLater(tick, 120);
7902
7941
  };
7903
- setTimeout(tick, 40);
7942
+ autofillLater(tick, 40);
7904
7943
  };
7905
7944
  const startAutofillWatch = () => {
7906
- if (autofillTimer) return;
7945
+ if (autofillDisposed || autofillTimer) return;
7907
7946
  autofillTimer = setInterval(() => {
7908
7947
  if (document.activeElement !== input) {
7909
7948
  clearInterval(autofillTimer);
@@ -7932,12 +7971,12 @@ const unitCover = (v, name = 'size') => {
7932
7971
  props.onFocus?.(e);
7933
7972
  startAutofillWatch();
7934
7973
  scheduleAutofillProbe();
7935
- setTimeout(syncAutofill, 50);
7974
+ autofillLater(syncAutofill, 50);
7936
7975
  });
7937
7976
  input.addEventListener("blur", (e) => {
7938
7977
  props.onBlur?.(e);
7939
7978
  stopAutofillWatch();
7940
- setTimeout(syncAutofill, 0);
7979
+ autofillLater(syncAutofill, 0);
7941
7980
  });
7942
7981
  input.addEventListener("animationstart", (e) => {
7943
7982
  if (e.animationName !== "cms-autofill") return;
@@ -7945,9 +7984,16 @@ const unitCover = (v, name = 'size') => {
7945
7984
  startAutofillWatch();
7946
7985
  scheduleAutofillProbe();
7947
7986
  });
7948
- setTimeout(syncAutofill, 0);
7987
+ autofillLater(syncAutofill, 0);
7949
7988
  scheduleAutofillProbe();
7950
7989
 
7990
+ JSswift._registerCleanup(input, () => {
7991
+ autofillDisposed = true;
7992
+ stopAutofillWatch();
7993
+ autofillTimeouts.forEach(clearTimeout);
7994
+ autofillTimeouts.clear();
7995
+ });
7996
+
7951
7997
  // model binding
7952
7998
  const model = props.model;
7953
7999
  if (model) {
@@ -7961,11 +8007,12 @@ const unitCover = (v, name = 'size') => {
7961
8007
  lastKnownValue = next;
7962
8008
  };
7963
8009
 
7964
- rod.action((v) => {
8010
+ const onValue = (v) => {
7965
8011
  if (syncing) return;
7966
8012
  syncing = true;
7967
8013
  try { updateFromRod(v); } finally { syncing = false; }
7968
- });
8014
+ };
8015
+ rod.action(onValue);
7969
8016
 
7970
8017
  // Component setup may run inside a dynamic parent render. This initial
7971
8018
  // hydration must not make that parent subscribe to the control model.
@@ -7982,18 +8029,26 @@ const unitCover = (v, name = 'size') => {
7982
8029
  input.addEventListener("input", onInput);
7983
8030
  input.addEventListener("compositionend", onInput);
7984
8031
 
7985
- if (typeof rod.onDispose === "function") {
7986
- rod.onDispose(() => {
7987
- input.removeEventListener("input", onInput);
7988
- input.removeEventListener("compositionend", onInput);
7989
- });
7990
- }
8032
+ let disposed = false;
8033
+ const stop = () => {
8034
+ if (disposed) return;
8035
+ disposed = true;
8036
+ input.removeEventListener("input", onInput);
8037
+ input.removeEventListener("compositionend", onInput);
8038
+ const actionIndex = rod._actions?.indexOf(onValue) ?? -1;
8039
+ if (actionIndex >= 0) rod._actions.splice(actionIndex, 1);
8040
+ const hookIndex = rod._disposeHooks?.indexOf(stop) ?? -1;
8041
+ if (hookIndex >= 0 && !rod._disposed) rod._disposeHooks.splice(hookIndex, 1);
8042
+ };
8043
+ rod.onDispose?.(stop);
8044
+ JSswift._registerCleanup(input, stop);
7991
8045
  };
7992
8046
  if (typeof model === "object" && typeof model._bind === "function") {
7993
8047
  bindInputRod(model);
7994
8048
  } else if (Array.isArray(model) && typeof model[0] === "function" && typeof model[1] === "function") {
7995
8049
  const r = JSswift.reactive.untracked(() => JSswift.rodFromSignal(model[0], model[1]));
7996
8050
  bindInputRod(r);
8051
+ JSswift._registerCleanup(input, () => r.dispose());
7997
8052
  }
7998
8053
  }
7999
8054
 
@@ -8077,11 +8132,12 @@ const unitCover = (v, name = 'size') => {
8077
8132
  textarea.value = next;
8078
8133
  };
8079
8134
 
8080
- rod.action((v) => {
8135
+ const onValue = (v) => {
8081
8136
  if (syncing) return;
8082
8137
  syncing = true;
8083
8138
  try { updateFromRod(v); } finally { syncing = false; }
8084
- });
8139
+ };
8140
+ rod.action(onValue);
8085
8141
 
8086
8142
  JSswift.reactive.untracked(() => updateFromRod(rod.value));
8087
8143
 
@@ -8096,18 +8152,26 @@ const unitCover = (v, name = 'size') => {
8096
8152
  textarea.addEventListener("input", onInput);
8097
8153
  textarea.addEventListener("compositionend", onInput);
8098
8154
 
8099
- if (typeof rod.onDispose === "function") {
8100
- rod.onDispose(() => {
8101
- textarea.removeEventListener("input", onInput);
8102
- textarea.removeEventListener("compositionend", onInput);
8103
- });
8104
- }
8155
+ let disposed = false;
8156
+ const stop = () => {
8157
+ if (disposed) return;
8158
+ disposed = true;
8159
+ textarea.removeEventListener("input", onInput);
8160
+ textarea.removeEventListener("compositionend", onInput);
8161
+ const actionIndex = rod._actions?.indexOf(onValue) ?? -1;
8162
+ if (actionIndex >= 0) rod._actions.splice(actionIndex, 1);
8163
+ const hookIndex = rod._disposeHooks?.indexOf(stop) ?? -1;
8164
+ if (hookIndex >= 0 && !rod._disposed) rod._disposeHooks.splice(hookIndex, 1);
8165
+ };
8166
+ rod.onDispose?.(stop);
8167
+ JSswift._registerCleanup(textarea, stop);
8105
8168
  };
8106
8169
  if (typeof model === "object" && typeof model._bind === "function") {
8107
8170
  bindTextareaRod(model);
8108
8171
  } else if (Array.isArray(model) && typeof model[0] === "function" && typeof model[1] === "function") {
8109
8172
  const r = JSswift.reactive.untracked(() => JSswift.rodFromSignal(model[0], model[1]));
8110
8173
  bindTextareaRod(r);
8174
+ JSswift._registerCleanup(textarea, () => r.dispose());
8111
8175
  }
8112
8176
  }
8113
8177
 
@@ -8143,7 +8207,22 @@ const unitCover = (v, name = 'size') => {
8143
8207
  };
8144
8208
 
8145
8209
 
8210
+ // Component-owned subscriptions; shared models remain caller-owned.
8211
+ function uiSubscriptionScope() {
8212
+ const stops = [];
8213
+ return {
8214
+ effect: (fn, name) => { const stop = JSswift.reactive.effect(fn, name); stops.push(stop); return stop; },
8215
+ action: (rod, fn) => {
8216
+ rod.action(fn);
8217
+ stops.push(() => { const i = rod._actions?.indexOf(fn) ?? -1; if (i >= 0) rod._actions.splice(i, 1); });
8218
+ },
8219
+ dispose: () => stops.splice(0).forEach(stop => stop())
8220
+ };
8221
+ }
8222
+
8146
8223
  UI.Search = (props = {}) => {
8224
+ const scope = uiSubscriptionScope();
8225
+ let disposed = false;
8147
8226
  const slots = props.slots || {};
8148
8227
  const isBoundValue = (v) => !!v && ((typeof v === "object" && typeof v._bind === "function") || uiIsSignal(v));
8149
8228
  const valueBinding = isBoundValue(props.model)
@@ -8256,6 +8335,7 @@ const unitCover = (v, name = 'size') => {
8256
8335
  setActive(-1);
8257
8336
  };
8258
8337
  const runSearch = async (query, options = {}) => {
8338
+ if (disposed) return;
8259
8339
  const q = query == null ? "" : String(query).trim();
8260
8340
  const id = ++requestId;
8261
8341
  const endpoint = uiUnwrap(props.endpoint ?? props.url);
@@ -8310,7 +8390,7 @@ const unitCover = (v, name = 'size') => {
8310
8390
  } else {
8311
8391
  raw = [];
8312
8392
  }
8313
- if (id !== requestId) return;
8393
+ if (disposed || id !== requestId) return;
8314
8394
  const mapped = typeof props.mapResponse === "function" ? props.mapResponse(raw, q) : normalizeResults(raw);
8315
8395
  const results = normalizeResults(mapped);
8316
8396
  if (cacheEnabled) cache.set(cacheKey, results);
@@ -8319,13 +8399,13 @@ const unitCover = (v, name = 'size') => {
8319
8399
  props.onResults?.(results, q);
8320
8400
  } catch (error) {
8321
8401
  if (error?.name === "AbortError") return;
8322
- if (id !== requestId) return;
8402
+ if (disposed || id !== requestId) return;
8323
8403
  setError(error);
8324
8404
  setResults([]);
8325
8405
  setActive(-1);
8326
8406
  props.onError?.(error, q);
8327
8407
  } finally {
8328
- if (id === requestId) {
8408
+ if (!disposed && id === requestId) {
8329
8409
  setLoading(false);
8330
8410
  activeController = null;
8331
8411
  scheduleMenuPosition();
@@ -8333,6 +8413,7 @@ const unitCover = (v, name = 'size') => {
8333
8413
  }
8334
8414
  };
8335
8415
  const scheduleSearch = (query, options = {}) => {
8416
+ if (disposed) return;
8336
8417
  if (searchTimer) clearTimeout(searchTimer);
8337
8418
  const delay = options.immediate ? 0 : debounceMs();
8338
8419
  searchTimer = setTimeout(() => {
@@ -8493,18 +8574,18 @@ const unitCover = (v, name = 'size') => {
8493
8574
 
8494
8575
  if (valueBinding) {
8495
8576
  if (typeof valueBinding === "object" && typeof valueBinding._bind === "function") {
8496
- valueBinding.action((v) => setQuery(v, { fromModel: true }));
8577
+ scope.action(valueBinding, (v) => setQuery(v, { fromModel: true }));
8497
8578
  modelSet = (v) => {
8498
8579
  if (valueBinding.value !== v) valueBinding.value = v;
8499
8580
  };
8500
8581
  } else if (Array.isArray(valueBinding) && typeof valueBinding[0] === "function" && typeof valueBinding[1] === "function") {
8501
8582
  const get = valueBinding[0];
8502
8583
  const set = valueBinding[1];
8503
- JSswift.reactive.effect(() => { setQuery(get(), { fromModel: true }); }, "UI.Search:model");
8584
+ scope.effect(() => { setQuery(get(), { fromModel: true }); }, "UI.Search:model");
8504
8585
  modelSet = (v) => set(v);
8505
8586
  }
8506
8587
  } else if (uiIsReactive(props.value)) {
8507
- JSswift.reactive.effect(() => { setQuery(uiUnwrap(props.value), { fromModel: true }); }, "UI.Search:value");
8588
+ scope.effect(() => { setQuery(uiUnwrap(props.value), { fromModel: true }); }, "UI.Search:value");
8508
8589
  }
8509
8590
 
8510
8591
  input.addEventListener("input", (event) => {
@@ -8578,7 +8659,7 @@ const unitCover = (v, name = 'size') => {
8578
8659
  };
8579
8660
  document.addEventListener("click", onDocClick, true);
8580
8661
 
8581
- JSswift.reactive.effect(() => {
8662
+ scope.effect(() => {
8582
8663
  const openNow = getOpen();
8583
8664
  root.classList.toggle("open", openNow);
8584
8665
  input.setAttribute("aria-expanded", openNow ? "true" : "false");
@@ -8586,7 +8667,7 @@ const unitCover = (v, name = 'size') => {
8586
8667
  else unmountMenuPortal();
8587
8668
  }, "UI.Search:open");
8588
8669
 
8589
- JSswift.reactive.effect(() => {
8670
+ scope.effect(() => {
8590
8671
  const d = isDisabled();
8591
8672
  const r = isReadonly();
8592
8673
  root.classList.toggle("disabled", d);
@@ -8595,7 +8676,7 @@ const unitCover = (v, name = 'size') => {
8595
8676
  input.readOnly = r;
8596
8677
  }, "UI.Search:disabled");
8597
8678
 
8598
- JSswift.reactive.effect(() => {
8679
+ scope.effect(() => {
8599
8680
  const loading = getLoading();
8600
8681
  const error = getError();
8601
8682
  const results = getResults();
@@ -8649,7 +8730,7 @@ const unitCover = (v, name = 'size') => {
8649
8730
  scheduleMenuPosition();
8650
8731
  }, "UI.Search:render");
8651
8732
 
8652
- JSswift.reactive.effect(() => {
8733
+ scope.effect(() => {
8653
8734
  const active = getActive();
8654
8735
  if (active === lastActive) return;
8655
8736
  if (resultNodes[lastActive]) {
@@ -8692,10 +8773,12 @@ const unitCover = (v, name = 'size') => {
8692
8773
  field._searchNow = () => runSearch(getQuery(), { immediate: true, open: true });
8693
8774
  field._getResults = getResults;
8694
8775
  field._setValue = (value) => setQuery(value, {});
8695
- let disposed = false;
8696
8776
  field._dispose = () => {
8697
8777
  if (disposed) return;
8698
8778
  disposed = true;
8779
+ scope.dispose();
8780
+ requestId++;
8781
+ cache.clear();
8699
8782
  document.removeEventListener("click", onDocClick, true);
8700
8783
  if (searchTimer) clearTimeout(searchTimer);
8701
8784
  if (activeController) activeController.abort();
@@ -8716,6 +8799,8 @@ const unitCover = (v, name = 'size') => {
8716
8799
 
8717
8800
 
8718
8801
  UI.Select = (props = {}) => {
8802
+ const scope = uiSubscriptionScope();
8803
+ let disposed = false;
8719
8804
  const fieldProps = {
8720
8805
  ...props,
8721
8806
  label: props.label ?? props.placeholder
@@ -8758,7 +8843,7 @@ const unitCover = (v, name = 'size') => {
8758
8843
  if (valueBinding) {
8759
8844
  if (typeof valueBinding === "object" && typeof valueBinding._bind === "function") {
8760
8845
  JSswift.reactive.untracked(() => setValue(normalizeValue(valueBinding.value)));
8761
- valueBinding.action((v) => setValue(normalizeValue(v)));
8846
+ scope.action(valueBinding, (v) => setValue(normalizeValue(v)));
8762
8847
  modelSet = (v) => {
8763
8848
  const next = normalizeValue(v);
8764
8849
  if (valueBinding.value !== next) valueBinding.value = next;
@@ -8767,7 +8852,7 @@ const unitCover = (v, name = 'size') => {
8767
8852
  const get = valueBinding[0];
8768
8853
  const set = valueBinding[1];
8769
8854
  JSswift.reactive.untracked(() => {
8770
- JSswift.reactive.effect(() => { setValue(normalizeValue(get())); }, "UI.Select:model");
8855
+ scope.effect(() => { setValue(normalizeValue(get())); }, "UI.Select:model");
8771
8856
  });
8772
8857
  modelSet = (v) => set(normalizeValue(v));
8773
8858
  }
@@ -8834,6 +8919,7 @@ const unitCover = (v, name = 'size') => {
8834
8919
 
8835
8920
  // supports async options
8836
8921
  async function loadOptions() {
8922
+ if (disposed) return;
8837
8923
  const src = props.options;
8838
8924
  let raw;
8839
8925
 
@@ -8847,12 +8933,13 @@ const unitCover = (v, name = 'size') => {
8847
8933
  } else {
8848
8934
  raw = v;
8849
8935
  }
8850
- buildLists(raw);
8936
+ if (!disposed) buildLists(raw);
8851
8937
  } catch (e) {
8938
+ if (disposed) return;
8852
8939
  console.error("[UI.Select] options load error:", e);
8853
8940
  buildLists([]);
8854
8941
  } finally {
8855
- setLoading(false);
8942
+ if (!disposed) setLoading(false);
8856
8943
  }
8857
8944
  }
8858
8945
 
@@ -8892,11 +8979,12 @@ const unitCover = (v, name = 'size') => {
8892
8979
  }
8893
8980
 
8894
8981
  async function open() {
8895
- if (isDisabled()) return;
8982
+ if (disposed || isDisabled()) return;
8896
8983
  setOpen(true);
8897
8984
 
8898
8985
  // load options (sync/async) on open
8899
8986
  await loadOptions();
8987
+ if (disposed) return;
8900
8988
 
8901
8989
  // set active to current selected, otherwise first enabled
8902
8990
  const current = getValue();
@@ -8934,7 +9022,7 @@ const unitCover = (v, name = 'size') => {
8934
9022
  const valueNode = _.div({
8935
9023
  class: uiClass(["cms-select-value", isMulti ? "cms-select-value-multi" : ""])
8936
9024
  });
8937
- JSswift.reactive.effect(() => {
9025
+ scope.effect(() => {
8938
9026
  const flat = getFlat();
8939
9027
  valueNode.innerHTML = "";
8940
9028
  if (isMulti) {
@@ -8981,7 +9069,7 @@ const unitCover = (v, name = 'size') => {
8981
9069
  }, "UI.Select:value");
8982
9070
 
8983
9071
  const arrowWrap = _.div({ class: "cms-select-arrow" });
8984
- JSswift.reactive.effect(() => {
9072
+ scope.effect(() => {
8985
9073
  const arrowNode = JSswift.ui.renderSlot(slots, "arrow", { open: getOpen() }, UI.Icon("#chevron-down"));
8986
9074
  arrowWrap.innerHTML = "";
8987
9075
  renderSlotToArray(null, "default", {}, arrowNode).forEach(n => arrowWrap.appendChild(n));
@@ -9023,7 +9111,7 @@ const unitCover = (v, name = 'size') => {
9023
9111
  if (filterInput) filterInput.value = getFilter() || "";
9024
9112
  };
9025
9113
  JSswift.reactive.untracked(renderFilterSlot);
9026
- JSswift.reactive.effect(() => {
9114
+ scope.effect(() => {
9027
9115
  const next = getFilter() || "";
9028
9116
  if (filterInput && "value" in filterInput && filterInput.value !== next) {
9029
9117
  filterInput.value = next;
@@ -9032,7 +9120,7 @@ const unitCover = (v, name = 'size') => {
9032
9120
 
9033
9121
  if (typeof filterable === "boolean") filterable ? filterWrap.classList.remove("cms-d-none") : filterWrap.classList.add("cms-d-none");
9034
9122
  else if (filterable?.action) {
9035
- filterable.action((v) => {
9123
+ scope.action(filterable, (v) => {
9036
9124
  v ? filterWrap.classList.remove("cms-d-none") : filterWrap.classList.add("cms-d-none");
9037
9125
  });
9038
9126
  filterable.value ? filterWrap.classList.remove("cms-d-none") : filterWrap.classList.add("cms-d-none");;
@@ -9133,7 +9221,7 @@ const unitCover = (v, name = 'size') => {
9133
9221
  root.appendChild(menu);
9134
9222
 
9135
9223
  // open class + aria
9136
- JSswift.reactive.effect(() => {
9224
+ scope.effect(() => {
9137
9225
  const o = getOpen();
9138
9226
  root.classList.toggle("open", o);
9139
9227
  root.setAttribute("aria-expanded", o ? "true" : "false");
@@ -9141,7 +9229,7 @@ const unitCover = (v, name = 'size') => {
9141
9229
  else unmountMenuPortal();
9142
9230
  }, "UI.Select:open");
9143
9231
 
9144
- JSswift.reactive.effect(() => {
9232
+ scope.effect(() => {
9145
9233
  const d = isDisabled();
9146
9234
  root.classList.toggle("disabled", d);
9147
9235
  root.tabIndex = d ? -1 : 0;
@@ -9163,7 +9251,7 @@ const unitCover = (v, name = 'size') => {
9163
9251
  if (active >= 0) optionNodes[active]?.scrollIntoView({ block: "nearest" });
9164
9252
  }
9165
9253
 
9166
- JSswift.reactive.effect(() => {
9254
+ scope.effect(() => {
9167
9255
  const display = getList();
9168
9256
  const flat = getFlat();
9169
9257
  const filter = (getFilter() || "").toLowerCase().trim();
@@ -9260,7 +9348,7 @@ const unitCover = (v, name = 'size') => {
9260
9348
  if (getOpen()) scheduleMenuPosition();
9261
9349
  }, "UI.Select:render");
9262
9350
 
9263
- JSswift.reactive.effect(() => {
9351
+ scope.effect(() => {
9264
9352
  paintActive();
9265
9353
  }, "UI.Select:paintActive");
9266
9354
 
@@ -9374,10 +9462,10 @@ const unitCover = (v, name = 'size') => {
9374
9462
  root.addEventListener("keydown", onKeyDown);
9375
9463
  filterWrap.addEventListener("keydown", onKeyDown);
9376
9464
 
9377
- let disposed = false;
9378
9465
  root._dispose = () => {
9379
9466
  if (disposed) return;
9380
9467
  disposed = true;
9468
+ scope.dispose();
9381
9469
  document.removeEventListener("click", onDocClick, true);
9382
9470
  root.removeEventListener("keydown", onKeyDown);
9383
9471
  filterWrap.removeEventListener("keydown", onKeyDown);
@@ -9417,7 +9505,7 @@ const unitCover = (v, name = 'size') => {
9417
9505
  });
9418
9506
 
9419
9507
  // refresh has-value when value changes
9420
- JSswift.reactive.effect(() => { field._refresh?.(); }, "UI.Select:fieldRefresh");
9508
+ scope.effect(() => { field._refresh?.(); }, "UI.Select:fieldRefresh");
9421
9509
 
9422
9510
  uiConfigureFieldControl(field, root, props);
9423
9511
  field._select = root;
@@ -10000,7 +10088,10 @@ const unitCover = (v, name = 'size') => {
10000
10088
  return {
10001
10089
  get: () => model.value,
10002
10090
  set: (v) => { if (model.value !== v) model.value = v; },
10003
- watch: (fn) => { if (typeof model.action === "function") model.action(fn); }
10091
+ watch: (fn) => {
10092
+ model.action?.(fn);
10093
+ return () => { const i = model._actions?.indexOf(fn) ?? -1; if (i >= 0) model._actions.splice(i, 1); };
10094
+ }
10004
10095
  };
10005
10096
  }
10006
10097
  if (Array.isArray(model) && typeof model[0] === "function" && typeof model[1] === "function") {
@@ -10178,7 +10269,7 @@ const unitCover = (v, name = 'size') => {
10178
10269
  if (getOpen() !== next) syncOpen(next);
10179
10270
  controlledDrawer?.set(next);
10180
10271
  };
10181
- controlledDrawer?.watch?.(() => {
10272
+ const stopDrawerWatch = controlledDrawer?.watch?.(() => {
10182
10273
  const next = !!controlledDrawer.get();
10183
10274
  if (getOpen() !== next) syncOpen(next);
10184
10275
  });
@@ -10193,7 +10284,7 @@ const unitCover = (v, name = 'size') => {
10193
10284
  if (getNavOpen() !== next) syncNavOpen(next);
10194
10285
  controlledNav?.set(next);
10195
10286
  };
10196
- controlledNav?.watch?.(() => {
10287
+ const stopNavWatch = controlledNav?.watch?.(() => {
10197
10288
  const next = !!controlledNav.get();
10198
10289
  if (getNavOpen() !== next) syncNavOpen(next);
10199
10290
  });
@@ -10281,6 +10372,8 @@ const unitCover = (v, name = 'size') => {
10281
10372
  JSswift.uiApplyResponsiveProps(p, props, JSswift.uiResponsiveStyleRules);
10282
10373
 
10283
10374
  const root = _.div(p);
10375
+ if (stopDrawerWatch) JSswift._registerCleanup(root, stopDrawerWatch);
10376
+ if (stopNavWatch) JSswift._registerCleanup(root, stopNavWatch);
10284
10377
  const tagPage = uiUnwrap(resolveLayoutProp("tagPage")) === true;
10285
10378
  const tags = tagPage
10286
10379
  ? { header: "header", aside: "aside", nav: "nav", page: "main", footer: "footer" }
@@ -21183,8 +21276,9 @@ transition: width 200ms ease;
21183
21276
  };
21184
21277
 
21185
21278
  let searchInput = null;
21279
+ let stopSearchWatch;
21186
21280
  if (searchModel) {
21187
- searchModel.watch((value) => {
21281
+ stopSearchWatch = searchModel.watch((value) => {
21188
21282
  const next = String(value ?? "");
21189
21283
  setQueryState(next);
21190
21284
  if (searchInput && searchInput.value !== next) searchInput.value = next;
@@ -21199,7 +21293,7 @@ transition: width 200ms ease;
21199
21293
  "actions", "actionsLabel", "emptyText", "loadingText", "onRowClick", "onRowDblClick",
21200
21294
  "tableClass", "tableStyle", "cardClass", "dense", "striped", "hover", "stickyHeader",
21201
21295
  "toolbar", "toolbarStart", "toolbarEnd", "caption", "footer", "status", "rowClass", "rowAttrs",
21202
- "minTableWidth", "hideHeader", "hideFooter", "slots", "body"
21296
+ "minTableWidth", "hideHeader", "hideFooter", "hideStatus", "slots", "body"
21203
21297
  ]);
21204
21298
  wrapProps.class = uiClass(["cms-table-card", props.class, props.cardClass]);
21205
21299
  if (props.dense != null) wrapProps.dense = props.dense;
@@ -21262,7 +21356,7 @@ transition: width 200ms ease;
21262
21356
  statusSort,
21263
21357
  ...statusExtraNodes
21264
21358
  );
21265
- shell.appendChild(status);
21359
+ if (props.hideStatus !== true) shell.appendChild(status);
21266
21360
 
21267
21361
  const captionNodes = renderSlotToArray(slots, "caption", {}, props.caption);
21268
21362
  if (captionNodes.length) shell.appendChild(_.div({ class: "cms-table-caption" }, ...captionNodes));
@@ -21394,7 +21488,7 @@ transition: width 200ms ease;
21394
21488
  );
21395
21489
  };
21396
21490
 
21397
- app.reactive.effect(() => {
21491
+ const stopRender = app.reactive.effect(() => {
21398
21492
  const rows = tableToArray(props.rows);
21399
21493
  const query = String(getQuery() || "").trim();
21400
21494
  const currentSort = getSort();
@@ -21568,7 +21662,10 @@ transition: width 200ms ease;
21568
21662
  }, "UI.Table:render");
21569
21663
 
21570
21664
  wrapProps.body = shell;
21571
- return UI.Card(wrapProps);
21665
+ const card = UI.Card(wrapProps);
21666
+ JSswift._registerCleanup(card, stopRender);
21667
+ if (stopSearchWatch) JSswift._registerCleanup(card, stopSearchWatch);
21668
+ return card;
21572
21669
  };
21573
21670
  UI.Menu = (...args) => {
21574
21671
  const { props, children } = JSswift.uiNormalizeArgs(args);