@jsswift/ui 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/ui.js CHANGED
@@ -2446,7 +2446,8 @@ const unitCover = (v, name = 'size') => {
2446
2446
  || typeof noteSlot === "function"
2447
2447
  || typeof hintSlot === "function";
2448
2448
  if (canReact) {
2449
- JSswift.reactive.effect(() => { renderMessage(); }, "UI.FormField:message");
2449
+ const stopMessage = JSswift.reactive.effect(() => { renderMessage(); }, "UI.FormField:message");
2450
+ JSswift._registerCleanup(wrap, stopMessage);
2450
2451
  } else {
2451
2452
  renderMessage();
2452
2453
  }
@@ -2469,6 +2470,16 @@ const unitCover = (v, name = 'size') => {
2469
2470
  value: props.value
2470
2471
  });
2471
2472
 
2473
+ let autofillDisposed = false;
2474
+ const autofillTimeouts = new Set();
2475
+ const autofillLater = (fn, delay) => {
2476
+ if (autofillDisposed) return;
2477
+ const id = setTimeout(() => {
2478
+ autofillTimeouts.delete(id);
2479
+ if (!autofillDisposed) fn();
2480
+ }, delay);
2481
+ autofillTimeouts.add(id);
2482
+ };
2472
2483
  let lastKnownValue = el.value ?? "";
2473
2484
  const syncAutofill = () => {
2474
2485
  const next = el.value ?? "";
@@ -2480,7 +2491,7 @@ const unitCover = (v, name = 'size') => {
2480
2491
  let autofillTimer = null;
2481
2492
  let autofillProbe = false;
2482
2493
  const scheduleAutofillProbe = () => {
2483
- if (autofillProbe) return;
2494
+ if (autofillDisposed || autofillProbe) return;
2484
2495
  autofillProbe = true;
2485
2496
  let count = 0;
2486
2497
  const tick = () => {
@@ -2490,12 +2501,12 @@ const unitCover = (v, name = 'size') => {
2490
2501
  autofillProbe = false;
2491
2502
  return;
2492
2503
  }
2493
- setTimeout(tick, 120);
2504
+ autofillLater(tick, 120);
2494
2505
  };
2495
- setTimeout(tick, 40);
2506
+ autofillLater(tick, 40);
2496
2507
  };
2497
2508
  const startAutofillWatch = () => {
2498
- if (autofillTimer) return;
2509
+ if (autofillDisposed || autofillTimer) return;
2499
2510
  autofillTimer = setInterval(() => {
2500
2511
  if (document.activeElement !== el) {
2501
2512
  clearInterval(autofillTimer);
@@ -2521,27 +2532,33 @@ const unitCover = (v, name = 'size') => {
2521
2532
  el.addEventListener("focus", () => {
2522
2533
  startAutofillWatch();
2523
2534
  scheduleAutofillProbe();
2524
- setTimeout(syncAutofill, 50);
2535
+ autofillLater(syncAutofill, 50);
2525
2536
  });
2526
2537
  el.addEventListener("blur", () => {
2527
2538
  stopAutofillWatch();
2528
- setTimeout(syncAutofill, 0);
2539
+ autofillLater(syncAutofill, 0);
2529
2540
  });
2530
- setTimeout(syncAutofill, 0);
2541
+ autofillLater(syncAutofill, 0);
2531
2542
  scheduleAutofillProbe();
2532
2543
 
2544
+ JSswift._registerCleanup(el, () => {
2545
+ autofillDisposed = true;
2546
+ stopAutofillWatch();
2547
+ autofillTimeouts.forEach(clearTimeout);
2548
+ autofillTimeouts.clear();
2549
+ });
2550
+
2533
2551
  // model binding
2534
2552
  const model = props.model;
2535
2553
  if (model) {
2536
2554
  // Supporta rod direttamente
2537
2555
  if (typeof model === "object" && typeof model._bind === "function") {
2538
- app.rodModel(el, model);
2556
+ JSswift._registerCleanup(el, app.rodModel(el, model));
2539
2557
  }
2540
2558
  // Supporta [get,set] signal -> creiamo rodFromSignal
2541
2559
  else if (Array.isArray(model) && typeof model[0] === "function" && typeof model[1] === "function") {
2542
- const r = app.rodFromSignal(model[0], model[1]);
2543
- app.rodModel(el, r);
2544
- // NOTE: se vuoi cleanup automatico qui, lo facciamo nel layer component (v2)
2560
+ const stop = JSswift.reactive.untracked(() => app.signalModel(el, model[0], model[1]));
2561
+ JSswift._registerCleanup(el, stop);
2545
2562
  }
2546
2563
  }
2547
2564
  uiRegisterShortcode(el, props, {
@@ -2558,7 +2575,7 @@ const unitCover = (v, name = 'size') => {
2558
2575
  if (value == null || value === false) control.removeAttribute(name);
2559
2576
  else control.setAttribute(name, String(value));
2560
2577
  };
2561
- JSswift.reactive.effect(() => {
2578
+ const stopAttributes = JSswift.reactive.effect(() => {
2562
2579
  const label = uiUnwrap(props.ariaLabel) ?? (typeof props.label === "string" ? props.label : null);
2563
2580
  set("aria-label", label);
2564
2581
  set("aria-labelledby", uiUnwrap(props.ariaLabelledBy));
@@ -2573,6 +2590,7 @@ const unitCover = (v, name = 'size') => {
2573
2590
  set(name.toLowerCase(), uiUnwrap(props[name]));
2574
2591
  }
2575
2592
  }, "UI.Field:controlAttributes");
2593
+ JSswift._registerCleanup(field, stopAttributes);
2576
2594
  }
2577
2595
 
2578
2596
  UI.Input = (props = {}) => {
@@ -2592,6 +2610,16 @@ const unitCover = (v, name = 'size') => {
2592
2610
  readOnly: !!props.readonly
2593
2611
  });
2594
2612
 
2613
+ let autofillDisposed = false;
2614
+ const autofillTimeouts = new Set();
2615
+ const autofillLater = (fn, delay) => {
2616
+ if (autofillDisposed) return;
2617
+ const id = setTimeout(() => {
2618
+ autofillTimeouts.delete(id);
2619
+ if (!autofillDisposed) fn();
2620
+ }, delay);
2621
+ autofillTimeouts.add(id);
2622
+ };
2595
2623
  let lastKnownValue = input.value ?? "";
2596
2624
  const syncAutofill = () => {
2597
2625
  const next = input.value ?? "";
@@ -2603,7 +2631,7 @@ const unitCover = (v, name = 'size') => {
2603
2631
  let autofillTimer = null;
2604
2632
  let autofillProbe = false;
2605
2633
  const scheduleAutofillProbe = () => {
2606
- if (autofillProbe) return;
2634
+ if (autofillDisposed || autofillProbe) return;
2607
2635
  autofillProbe = true;
2608
2636
  let count = 0;
2609
2637
  const tick = () => {
@@ -2613,12 +2641,12 @@ const unitCover = (v, name = 'size') => {
2613
2641
  autofillProbe = false;
2614
2642
  return;
2615
2643
  }
2616
- setTimeout(tick, 120);
2644
+ autofillLater(tick, 120);
2617
2645
  };
2618
- setTimeout(tick, 40);
2646
+ autofillLater(tick, 40);
2619
2647
  };
2620
2648
  const startAutofillWatch = () => {
2621
- if (autofillTimer) return;
2649
+ if (autofillDisposed || autofillTimer) return;
2622
2650
  autofillTimer = setInterval(() => {
2623
2651
  if (document.activeElement !== input) {
2624
2652
  clearInterval(autofillTimer);
@@ -2647,12 +2675,12 @@ const unitCover = (v, name = 'size') => {
2647
2675
  props.onFocus?.(e);
2648
2676
  startAutofillWatch();
2649
2677
  scheduleAutofillProbe();
2650
- setTimeout(syncAutofill, 50);
2678
+ autofillLater(syncAutofill, 50);
2651
2679
  });
2652
2680
  input.addEventListener("blur", (e) => {
2653
2681
  props.onBlur?.(e);
2654
2682
  stopAutofillWatch();
2655
- setTimeout(syncAutofill, 0);
2683
+ autofillLater(syncAutofill, 0);
2656
2684
  });
2657
2685
  input.addEventListener("animationstart", (e) => {
2658
2686
  if (e.animationName !== "cms-autofill") return;
@@ -2660,9 +2688,16 @@ const unitCover = (v, name = 'size') => {
2660
2688
  startAutofillWatch();
2661
2689
  scheduleAutofillProbe();
2662
2690
  });
2663
- setTimeout(syncAutofill, 0);
2691
+ autofillLater(syncAutofill, 0);
2664
2692
  scheduleAutofillProbe();
2665
2693
 
2694
+ JSswift._registerCleanup(input, () => {
2695
+ autofillDisposed = true;
2696
+ stopAutofillWatch();
2697
+ autofillTimeouts.forEach(clearTimeout);
2698
+ autofillTimeouts.clear();
2699
+ });
2700
+
2666
2701
  // model binding
2667
2702
  const model = props.model;
2668
2703
  if (model) {
@@ -2676,11 +2711,12 @@ const unitCover = (v, name = 'size') => {
2676
2711
  lastKnownValue = next;
2677
2712
  };
2678
2713
 
2679
- rod.action((v) => {
2714
+ const onValue = (v) => {
2680
2715
  if (syncing) return;
2681
2716
  syncing = true;
2682
2717
  try { updateFromRod(v); } finally { syncing = false; }
2683
- });
2718
+ };
2719
+ rod.action(onValue);
2684
2720
 
2685
2721
  // Component setup may run inside a dynamic parent render. This initial
2686
2722
  // hydration must not make that parent subscribe to the control model.
@@ -2697,18 +2733,26 @@ const unitCover = (v, name = 'size') => {
2697
2733
  input.addEventListener("input", onInput);
2698
2734
  input.addEventListener("compositionend", onInput);
2699
2735
 
2700
- if (typeof rod.onDispose === "function") {
2701
- rod.onDispose(() => {
2702
- input.removeEventListener("input", onInput);
2703
- input.removeEventListener("compositionend", onInput);
2704
- });
2705
- }
2736
+ let disposed = false;
2737
+ const stop = () => {
2738
+ if (disposed) return;
2739
+ disposed = true;
2740
+ input.removeEventListener("input", onInput);
2741
+ input.removeEventListener("compositionend", onInput);
2742
+ const actionIndex = rod._actions?.indexOf(onValue) ?? -1;
2743
+ if (actionIndex >= 0) rod._actions.splice(actionIndex, 1);
2744
+ const hookIndex = rod._disposeHooks?.indexOf(stop) ?? -1;
2745
+ if (hookIndex >= 0 && !rod._disposed) rod._disposeHooks.splice(hookIndex, 1);
2746
+ };
2747
+ rod.onDispose?.(stop);
2748
+ JSswift._registerCleanup(input, stop);
2706
2749
  };
2707
2750
  if (typeof model === "object" && typeof model._bind === "function") {
2708
2751
  bindInputRod(model);
2709
2752
  } else if (Array.isArray(model) && typeof model[0] === "function" && typeof model[1] === "function") {
2710
2753
  const r = JSswift.reactive.untracked(() => JSswift.rodFromSignal(model[0], model[1]));
2711
2754
  bindInputRod(r);
2755
+ JSswift._registerCleanup(input, () => r.dispose());
2712
2756
  }
2713
2757
  }
2714
2758
 
@@ -2792,11 +2836,12 @@ const unitCover = (v, name = 'size') => {
2792
2836
  textarea.value = next;
2793
2837
  };
2794
2838
 
2795
- rod.action((v) => {
2839
+ const onValue = (v) => {
2796
2840
  if (syncing) return;
2797
2841
  syncing = true;
2798
2842
  try { updateFromRod(v); } finally { syncing = false; }
2799
- });
2843
+ };
2844
+ rod.action(onValue);
2800
2845
 
2801
2846
  JSswift.reactive.untracked(() => updateFromRod(rod.value));
2802
2847
 
@@ -2811,18 +2856,26 @@ const unitCover = (v, name = 'size') => {
2811
2856
  textarea.addEventListener("input", onInput);
2812
2857
  textarea.addEventListener("compositionend", onInput);
2813
2858
 
2814
- if (typeof rod.onDispose === "function") {
2815
- rod.onDispose(() => {
2816
- textarea.removeEventListener("input", onInput);
2817
- textarea.removeEventListener("compositionend", onInput);
2818
- });
2819
- }
2859
+ let disposed = false;
2860
+ const stop = () => {
2861
+ if (disposed) return;
2862
+ disposed = true;
2863
+ textarea.removeEventListener("input", onInput);
2864
+ textarea.removeEventListener("compositionend", onInput);
2865
+ const actionIndex = rod._actions?.indexOf(onValue) ?? -1;
2866
+ if (actionIndex >= 0) rod._actions.splice(actionIndex, 1);
2867
+ const hookIndex = rod._disposeHooks?.indexOf(stop) ?? -1;
2868
+ if (hookIndex >= 0 && !rod._disposed) rod._disposeHooks.splice(hookIndex, 1);
2869
+ };
2870
+ rod.onDispose?.(stop);
2871
+ JSswift._registerCleanup(textarea, stop);
2820
2872
  };
2821
2873
  if (typeof model === "object" && typeof model._bind === "function") {
2822
2874
  bindTextareaRod(model);
2823
2875
  } else if (Array.isArray(model) && typeof model[0] === "function" && typeof model[1] === "function") {
2824
2876
  const r = JSswift.reactive.untracked(() => JSswift.rodFromSignal(model[0], model[1]));
2825
2877
  bindTextareaRod(r);
2878
+ JSswift._registerCleanup(textarea, () => r.dispose());
2826
2879
  }
2827
2880
  }
2828
2881
 
@@ -2858,7 +2911,22 @@ const unitCover = (v, name = 'size') => {
2858
2911
  };
2859
2912
 
2860
2913
 
2914
+ // Component-owned subscriptions; shared models remain caller-owned.
2915
+ function uiSubscriptionScope() {
2916
+ const stops = [];
2917
+ return {
2918
+ effect: (fn, name) => { const stop = JSswift.reactive.effect(fn, name); stops.push(stop); return stop; },
2919
+ action: (rod, fn) => {
2920
+ rod.action(fn);
2921
+ stops.push(() => { const i = rod._actions?.indexOf(fn) ?? -1; if (i >= 0) rod._actions.splice(i, 1); });
2922
+ },
2923
+ dispose: () => stops.splice(0).forEach(stop => stop())
2924
+ };
2925
+ }
2926
+
2861
2927
  UI.Search = (props = {}) => {
2928
+ const scope = uiSubscriptionScope();
2929
+ let disposed = false;
2862
2930
  const slots = props.slots || {};
2863
2931
  const isBoundValue = (v) => !!v && ((typeof v === "object" && typeof v._bind === "function") || uiIsSignal(v));
2864
2932
  const valueBinding = isBoundValue(props.model)
@@ -2971,6 +3039,7 @@ const unitCover = (v, name = 'size') => {
2971
3039
  setActive(-1);
2972
3040
  };
2973
3041
  const runSearch = async (query, options = {}) => {
3042
+ if (disposed) return;
2974
3043
  const q = query == null ? "" : String(query).trim();
2975
3044
  const id = ++requestId;
2976
3045
  const endpoint = uiUnwrap(props.endpoint ?? props.url);
@@ -3025,7 +3094,7 @@ const unitCover = (v, name = 'size') => {
3025
3094
  } else {
3026
3095
  raw = [];
3027
3096
  }
3028
- if (id !== requestId) return;
3097
+ if (disposed || id !== requestId) return;
3029
3098
  const mapped = typeof props.mapResponse === "function" ? props.mapResponse(raw, q) : normalizeResults(raw);
3030
3099
  const results = normalizeResults(mapped);
3031
3100
  if (cacheEnabled) cache.set(cacheKey, results);
@@ -3034,13 +3103,13 @@ const unitCover = (v, name = 'size') => {
3034
3103
  props.onResults?.(results, q);
3035
3104
  } catch (error) {
3036
3105
  if (error?.name === "AbortError") return;
3037
- if (id !== requestId) return;
3106
+ if (disposed || id !== requestId) return;
3038
3107
  setError(error);
3039
3108
  setResults([]);
3040
3109
  setActive(-1);
3041
3110
  props.onError?.(error, q);
3042
3111
  } finally {
3043
- if (id === requestId) {
3112
+ if (!disposed && id === requestId) {
3044
3113
  setLoading(false);
3045
3114
  activeController = null;
3046
3115
  scheduleMenuPosition();
@@ -3048,6 +3117,7 @@ const unitCover = (v, name = 'size') => {
3048
3117
  }
3049
3118
  };
3050
3119
  const scheduleSearch = (query, options = {}) => {
3120
+ if (disposed) return;
3051
3121
  if (searchTimer) clearTimeout(searchTimer);
3052
3122
  const delay = options.immediate ? 0 : debounceMs();
3053
3123
  searchTimer = setTimeout(() => {
@@ -3208,18 +3278,18 @@ const unitCover = (v, name = 'size') => {
3208
3278
 
3209
3279
  if (valueBinding) {
3210
3280
  if (typeof valueBinding === "object" && typeof valueBinding._bind === "function") {
3211
- valueBinding.action((v) => setQuery(v, { fromModel: true }));
3281
+ scope.action(valueBinding, (v) => setQuery(v, { fromModel: true }));
3212
3282
  modelSet = (v) => {
3213
3283
  if (valueBinding.value !== v) valueBinding.value = v;
3214
3284
  };
3215
3285
  } else if (Array.isArray(valueBinding) && typeof valueBinding[0] === "function" && typeof valueBinding[1] === "function") {
3216
3286
  const get = valueBinding[0];
3217
3287
  const set = valueBinding[1];
3218
- JSswift.reactive.effect(() => { setQuery(get(), { fromModel: true }); }, "UI.Search:model");
3288
+ scope.effect(() => { setQuery(get(), { fromModel: true }); }, "UI.Search:model");
3219
3289
  modelSet = (v) => set(v);
3220
3290
  }
3221
3291
  } else if (uiIsReactive(props.value)) {
3222
- JSswift.reactive.effect(() => { setQuery(uiUnwrap(props.value), { fromModel: true }); }, "UI.Search:value");
3292
+ scope.effect(() => { setQuery(uiUnwrap(props.value), { fromModel: true }); }, "UI.Search:value");
3223
3293
  }
3224
3294
 
3225
3295
  input.addEventListener("input", (event) => {
@@ -3293,7 +3363,7 @@ const unitCover = (v, name = 'size') => {
3293
3363
  };
3294
3364
  document.addEventListener("click", onDocClick, true);
3295
3365
 
3296
- JSswift.reactive.effect(() => {
3366
+ scope.effect(() => {
3297
3367
  const openNow = getOpen();
3298
3368
  root.classList.toggle("open", openNow);
3299
3369
  input.setAttribute("aria-expanded", openNow ? "true" : "false");
@@ -3301,7 +3371,7 @@ const unitCover = (v, name = 'size') => {
3301
3371
  else unmountMenuPortal();
3302
3372
  }, "UI.Search:open");
3303
3373
 
3304
- JSswift.reactive.effect(() => {
3374
+ scope.effect(() => {
3305
3375
  const d = isDisabled();
3306
3376
  const r = isReadonly();
3307
3377
  root.classList.toggle("disabled", d);
@@ -3310,7 +3380,7 @@ const unitCover = (v, name = 'size') => {
3310
3380
  input.readOnly = r;
3311
3381
  }, "UI.Search:disabled");
3312
3382
 
3313
- JSswift.reactive.effect(() => {
3383
+ scope.effect(() => {
3314
3384
  const loading = getLoading();
3315
3385
  const error = getError();
3316
3386
  const results = getResults();
@@ -3364,7 +3434,7 @@ const unitCover = (v, name = 'size') => {
3364
3434
  scheduleMenuPosition();
3365
3435
  }, "UI.Search:render");
3366
3436
 
3367
- JSswift.reactive.effect(() => {
3437
+ scope.effect(() => {
3368
3438
  const active = getActive();
3369
3439
  if (active === lastActive) return;
3370
3440
  if (resultNodes[lastActive]) {
@@ -3407,10 +3477,12 @@ const unitCover = (v, name = 'size') => {
3407
3477
  field._searchNow = () => runSearch(getQuery(), { immediate: true, open: true });
3408
3478
  field._getResults = getResults;
3409
3479
  field._setValue = (value) => setQuery(value, {});
3410
- let disposed = false;
3411
3480
  field._dispose = () => {
3412
3481
  if (disposed) return;
3413
3482
  disposed = true;
3483
+ scope.dispose();
3484
+ requestId++;
3485
+ cache.clear();
3414
3486
  document.removeEventListener("click", onDocClick, true);
3415
3487
  if (searchTimer) clearTimeout(searchTimer);
3416
3488
  if (activeController) activeController.abort();
@@ -3431,6 +3503,8 @@ const unitCover = (v, name = 'size') => {
3431
3503
 
3432
3504
 
3433
3505
  UI.Select = (props = {}) => {
3506
+ const scope = uiSubscriptionScope();
3507
+ let disposed = false;
3434
3508
  const fieldProps = {
3435
3509
  ...props,
3436
3510
  label: props.label ?? props.placeholder
@@ -3473,7 +3547,7 @@ const unitCover = (v, name = 'size') => {
3473
3547
  if (valueBinding) {
3474
3548
  if (typeof valueBinding === "object" && typeof valueBinding._bind === "function") {
3475
3549
  JSswift.reactive.untracked(() => setValue(normalizeValue(valueBinding.value)));
3476
- valueBinding.action((v) => setValue(normalizeValue(v)));
3550
+ scope.action(valueBinding, (v) => setValue(normalizeValue(v)));
3477
3551
  modelSet = (v) => {
3478
3552
  const next = normalizeValue(v);
3479
3553
  if (valueBinding.value !== next) valueBinding.value = next;
@@ -3482,7 +3556,7 @@ const unitCover = (v, name = 'size') => {
3482
3556
  const get = valueBinding[0];
3483
3557
  const set = valueBinding[1];
3484
3558
  JSswift.reactive.untracked(() => {
3485
- JSswift.reactive.effect(() => { setValue(normalizeValue(get())); }, "UI.Select:model");
3559
+ scope.effect(() => { setValue(normalizeValue(get())); }, "UI.Select:model");
3486
3560
  });
3487
3561
  modelSet = (v) => set(normalizeValue(v));
3488
3562
  }
@@ -3549,6 +3623,7 @@ const unitCover = (v, name = 'size') => {
3549
3623
 
3550
3624
  // supports async options
3551
3625
  async function loadOptions() {
3626
+ if (disposed) return;
3552
3627
  const src = props.options;
3553
3628
  let raw;
3554
3629
 
@@ -3562,12 +3637,13 @@ const unitCover = (v, name = 'size') => {
3562
3637
  } else {
3563
3638
  raw = v;
3564
3639
  }
3565
- buildLists(raw);
3640
+ if (!disposed) buildLists(raw);
3566
3641
  } catch (e) {
3642
+ if (disposed) return;
3567
3643
  console.error("[UI.Select] options load error:", e);
3568
3644
  buildLists([]);
3569
3645
  } finally {
3570
- setLoading(false);
3646
+ if (!disposed) setLoading(false);
3571
3647
  }
3572
3648
  }
3573
3649
 
@@ -3607,11 +3683,12 @@ const unitCover = (v, name = 'size') => {
3607
3683
  }
3608
3684
 
3609
3685
  async function open() {
3610
- if (isDisabled()) return;
3686
+ if (disposed || isDisabled()) return;
3611
3687
  setOpen(true);
3612
3688
 
3613
3689
  // load options (sync/async) on open
3614
3690
  await loadOptions();
3691
+ if (disposed) return;
3615
3692
 
3616
3693
  // set active to current selected, otherwise first enabled
3617
3694
  const current = getValue();
@@ -3649,7 +3726,7 @@ const unitCover = (v, name = 'size') => {
3649
3726
  const valueNode = _.div({
3650
3727
  class: uiClass(["cms-select-value", isMulti ? "cms-select-value-multi" : ""])
3651
3728
  });
3652
- JSswift.reactive.effect(() => {
3729
+ scope.effect(() => {
3653
3730
  const flat = getFlat();
3654
3731
  valueNode.innerHTML = "";
3655
3732
  if (isMulti) {
@@ -3696,7 +3773,7 @@ const unitCover = (v, name = 'size') => {
3696
3773
  }, "UI.Select:value");
3697
3774
 
3698
3775
  const arrowWrap = _.div({ class: "cms-select-arrow" });
3699
- JSswift.reactive.effect(() => {
3776
+ scope.effect(() => {
3700
3777
  const arrowNode = JSswift.ui.renderSlot(slots, "arrow", { open: getOpen() }, UI.Icon("#chevron-down"));
3701
3778
  arrowWrap.innerHTML = "";
3702
3779
  renderSlotToArray(null, "default", {}, arrowNode).forEach(n => arrowWrap.appendChild(n));
@@ -3738,7 +3815,7 @@ const unitCover = (v, name = 'size') => {
3738
3815
  if (filterInput) filterInput.value = getFilter() || "";
3739
3816
  };
3740
3817
  JSswift.reactive.untracked(renderFilterSlot);
3741
- JSswift.reactive.effect(() => {
3818
+ scope.effect(() => {
3742
3819
  const next = getFilter() || "";
3743
3820
  if (filterInput && "value" in filterInput && filterInput.value !== next) {
3744
3821
  filterInput.value = next;
@@ -3747,7 +3824,7 @@ const unitCover = (v, name = 'size') => {
3747
3824
 
3748
3825
  if (typeof filterable === "boolean") filterable ? filterWrap.classList.remove("cms-d-none") : filterWrap.classList.add("cms-d-none");
3749
3826
  else if (filterable?.action) {
3750
- filterable.action((v) => {
3827
+ scope.action(filterable, (v) => {
3751
3828
  v ? filterWrap.classList.remove("cms-d-none") : filterWrap.classList.add("cms-d-none");
3752
3829
  });
3753
3830
  filterable.value ? filterWrap.classList.remove("cms-d-none") : filterWrap.classList.add("cms-d-none");;
@@ -3848,7 +3925,7 @@ const unitCover = (v, name = 'size') => {
3848
3925
  root.appendChild(menu);
3849
3926
 
3850
3927
  // open class + aria
3851
- JSswift.reactive.effect(() => {
3928
+ scope.effect(() => {
3852
3929
  const o = getOpen();
3853
3930
  root.classList.toggle("open", o);
3854
3931
  root.setAttribute("aria-expanded", o ? "true" : "false");
@@ -3856,7 +3933,7 @@ const unitCover = (v, name = 'size') => {
3856
3933
  else unmountMenuPortal();
3857
3934
  }, "UI.Select:open");
3858
3935
 
3859
- JSswift.reactive.effect(() => {
3936
+ scope.effect(() => {
3860
3937
  const d = isDisabled();
3861
3938
  root.classList.toggle("disabled", d);
3862
3939
  root.tabIndex = d ? -1 : 0;
@@ -3878,7 +3955,7 @@ const unitCover = (v, name = 'size') => {
3878
3955
  if (active >= 0) optionNodes[active]?.scrollIntoView({ block: "nearest" });
3879
3956
  }
3880
3957
 
3881
- JSswift.reactive.effect(() => {
3958
+ scope.effect(() => {
3882
3959
  const display = getList();
3883
3960
  const flat = getFlat();
3884
3961
  const filter = (getFilter() || "").toLowerCase().trim();
@@ -3975,7 +4052,7 @@ const unitCover = (v, name = 'size') => {
3975
4052
  if (getOpen()) scheduleMenuPosition();
3976
4053
  }, "UI.Select:render");
3977
4054
 
3978
- JSswift.reactive.effect(() => {
4055
+ scope.effect(() => {
3979
4056
  paintActive();
3980
4057
  }, "UI.Select:paintActive");
3981
4058
 
@@ -4089,10 +4166,10 @@ const unitCover = (v, name = 'size') => {
4089
4166
  root.addEventListener("keydown", onKeyDown);
4090
4167
  filterWrap.addEventListener("keydown", onKeyDown);
4091
4168
 
4092
- let disposed = false;
4093
4169
  root._dispose = () => {
4094
4170
  if (disposed) return;
4095
4171
  disposed = true;
4172
+ scope.dispose();
4096
4173
  document.removeEventListener("click", onDocClick, true);
4097
4174
  root.removeEventListener("keydown", onKeyDown);
4098
4175
  filterWrap.removeEventListener("keydown", onKeyDown);
@@ -4132,7 +4209,7 @@ const unitCover = (v, name = 'size') => {
4132
4209
  });
4133
4210
 
4134
4211
  // refresh has-value when value changes
4135
- JSswift.reactive.effect(() => { field._refresh?.(); }, "UI.Select:fieldRefresh");
4212
+ scope.effect(() => { field._refresh?.(); }, "UI.Select:fieldRefresh");
4136
4213
 
4137
4214
  uiConfigureFieldControl(field, root, props);
4138
4215
  field._select = root;
@@ -4715,7 +4792,10 @@ const unitCover = (v, name = 'size') => {
4715
4792
  return {
4716
4793
  get: () => model.value,
4717
4794
  set: (v) => { if (model.value !== v) model.value = v; },
4718
- watch: (fn) => { if (typeof model.action === "function") model.action(fn); }
4795
+ watch: (fn) => {
4796
+ model.action?.(fn);
4797
+ return () => { const i = model._actions?.indexOf(fn) ?? -1; if (i >= 0) model._actions.splice(i, 1); };
4798
+ }
4719
4799
  };
4720
4800
  }
4721
4801
  if (Array.isArray(model) && typeof model[0] === "function" && typeof model[1] === "function") {
@@ -4893,7 +4973,7 @@ const unitCover = (v, name = 'size') => {
4893
4973
  if (getOpen() !== next) syncOpen(next);
4894
4974
  controlledDrawer?.set(next);
4895
4975
  };
4896
- controlledDrawer?.watch?.(() => {
4976
+ const stopDrawerWatch = controlledDrawer?.watch?.(() => {
4897
4977
  const next = !!controlledDrawer.get();
4898
4978
  if (getOpen() !== next) syncOpen(next);
4899
4979
  });
@@ -4908,7 +4988,7 @@ const unitCover = (v, name = 'size') => {
4908
4988
  if (getNavOpen() !== next) syncNavOpen(next);
4909
4989
  controlledNav?.set(next);
4910
4990
  };
4911
- controlledNav?.watch?.(() => {
4991
+ const stopNavWatch = controlledNav?.watch?.(() => {
4912
4992
  const next = !!controlledNav.get();
4913
4993
  if (getNavOpen() !== next) syncNavOpen(next);
4914
4994
  });
@@ -4996,6 +5076,8 @@ const unitCover = (v, name = 'size') => {
4996
5076
  JSswift.uiApplyResponsiveProps(p, props, JSswift.uiResponsiveStyleRules);
4997
5077
 
4998
5078
  const root = _.div(p);
5079
+ if (stopDrawerWatch) JSswift._registerCleanup(root, stopDrawerWatch);
5080
+ if (stopNavWatch) JSswift._registerCleanup(root, stopNavWatch);
4999
5081
  const tagPage = uiUnwrap(resolveLayoutProp("tagPage")) === true;
5000
5082
  const tags = tagPage
5001
5083
  ? { header: "header", aside: "aside", nav: "nav", page: "main", footer: "footer" }
@@ -15898,8 +15980,9 @@ transition: width 200ms ease;
15898
15980
  };
15899
15981
 
15900
15982
  let searchInput = null;
15983
+ let stopSearchWatch;
15901
15984
  if (searchModel) {
15902
- searchModel.watch((value) => {
15985
+ stopSearchWatch = searchModel.watch((value) => {
15903
15986
  const next = String(value ?? "");
15904
15987
  setQueryState(next);
15905
15988
  if (searchInput && searchInput.value !== next) searchInput.value = next;
@@ -15914,7 +15997,7 @@ transition: width 200ms ease;
15914
15997
  "actions", "actionsLabel", "emptyText", "loadingText", "onRowClick", "onRowDblClick",
15915
15998
  "tableClass", "tableStyle", "cardClass", "dense", "striped", "hover", "stickyHeader",
15916
15999
  "toolbar", "toolbarStart", "toolbarEnd", "caption", "footer", "status", "rowClass", "rowAttrs",
15917
- "minTableWidth", "hideHeader", "hideFooter", "slots", "body"
16000
+ "minTableWidth", "hideHeader", "hideFooter", "hideStatus", "slots", "body"
15918
16001
  ]);
15919
16002
  wrapProps.class = uiClass(["cms-table-card", props.class, props.cardClass]);
15920
16003
  if (props.dense != null) wrapProps.dense = props.dense;
@@ -15977,7 +16060,7 @@ transition: width 200ms ease;
15977
16060
  statusSort,
15978
16061
  ...statusExtraNodes
15979
16062
  );
15980
- shell.appendChild(status);
16063
+ if (props.hideStatus !== true) shell.appendChild(status);
15981
16064
 
15982
16065
  const captionNodes = renderSlotToArray(slots, "caption", {}, props.caption);
15983
16066
  if (captionNodes.length) shell.appendChild(_.div({ class: "cms-table-caption" }, ...captionNodes));
@@ -16109,7 +16192,7 @@ transition: width 200ms ease;
16109
16192
  );
16110
16193
  };
16111
16194
 
16112
- app.reactive.effect(() => {
16195
+ const stopRender = app.reactive.effect(() => {
16113
16196
  const rows = tableToArray(props.rows);
16114
16197
  const query = String(getQuery() || "").trim();
16115
16198
  const currentSort = getSort();
@@ -16283,7 +16366,10 @@ transition: width 200ms ease;
16283
16366
  }, "UI.Table:render");
16284
16367
 
16285
16368
  wrapProps.body = shell;
16286
- return UI.Card(wrapProps);
16369
+ const card = UI.Card(wrapProps);
16370
+ JSswift._registerCleanup(card, stopRender);
16371
+ if (stopSearchWatch) JSswift._registerCleanup(card, stopSearchWatch);
16372
+ return card;
16287
16373
  };
16288
16374
  UI.Menu = (...args) => {
16289
16375
  const { props, children } = JSswift.uiNormalizeArgs(args);