mnfst 0.5.261 → 0.5.262

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.261';
5
+ const MANIFEST_BUILD_VERSION = '0.5.262';
6
6
 
7
7
  /* Manifest Data Sources - Appwrite Integration */
8
8
 
@@ -1068,6 +1068,8 @@ function initializeColorpickerPlugin() {
1068
1068
 
1069
1069
  const state = {
1070
1070
  rootEl,
1071
+ _effects: [],
1072
+ _observers: [],
1071
1073
  pickerUid,
1072
1074
 
1073
1075
  // Data
@@ -1615,9 +1617,9 @@ function initializeColorpickerPlugin() {
1615
1617
  if (this.libraryRootValue && window.Alpine?.effect && window.Alpine?.evaluateLater) {
1616
1618
  // Explicit expression — bypass discovery, re-render on dep change.
1617
1619
  const evalFn = Alpine.evaluateLater(evalHost, this.libraryRootValue);
1618
- Alpine.effect(() => {
1620
+ this._effects.push(Alpine.effect(() => {
1619
1621
  evalFn(v => { this._libraryResolvedData = v; this._doRenderLibrary(); });
1620
- });
1622
+ }));
1621
1623
  } else if (window.Alpine?.effect) {
1622
1624
  // Zero-config — scan $x.manifest.data for `colorpicker:`-flagged
1623
1625
  // entries, collect their $x.<name> values, merge in declaration
@@ -1688,7 +1690,7 @@ function initializeColorpickerPlugin() {
1688
1690
  };
1689
1691
 
1690
1692
  // Reactive deps trigger re-runs on locale switch / manifest load.
1691
- Alpine.effect(() => {
1693
+ this._effects.push(Alpine.effect(() => {
1692
1694
  try { Alpine.evaluate(evalCtx, 'typeof $locale !== "undefined" && $locale.current'); } catch {}
1693
1695
  try { Alpine.evaluate(evalCtx, '$x && $x.manifest && $x.manifest._loadedFrom'); } catch {}
1694
1696
  runDiscovery();
@@ -1705,7 +1707,7 @@ function initializeColorpickerPlugin() {
1705
1707
  }
1706
1708
  }, 150);
1707
1709
  }
1708
- });
1710
+ }));
1709
1711
  } else {
1710
1712
  this._doRenderLibrary();
1711
1713
  }
@@ -2022,6 +2024,7 @@ function initializeColorpickerPlugin() {
2022
2024
  if (r.width > 0 && r.height > 0) { self.drawCanvas(); self.updateCanvasMarker(); }
2023
2025
  });
2024
2026
  ro.observe(refs.canvas);
2027
+ self._observers.push(ro);
2025
2028
  }
2026
2029
 
2027
2030
  if (refs.hueSlider) {
@@ -2453,10 +2456,17 @@ function initializeColorpickerPlugin() {
2453
2456
 
2454
2457
  // Defer mount so child directives register first. setTimeout (not rAF)
2455
2458
  // so it still fires when backgrounded.
2456
- setTimeout(() => state.mount(), 0);
2459
+ setTimeout(() => { if (!state._destroyed) state.mount(); }, 0);
2457
2460
 
2458
2461
  cleanup(() => {
2459
2462
  if (el.id) delete _pickerRegistry[el.id];
2463
+ state._destroyed = true;
2464
+ state._effects.forEach((fx) => Alpine.release(fx));
2465
+ state._effects.length = 0;
2466
+ state._observers.forEach((ro) => ro.disconnect());
2467
+ state._observers.length = 0;
2468
+ clearInterval(state._libraryPollTimer);
2469
+ state._libraryPollTimer = null;
2460
2470
  delete el._colorpickerState;
2461
2471
  });
2462
2472
  return;
@@ -2468,18 +2478,12 @@ function initializeColorpickerPlugin() {
2468
2478
  // Swatches trigger OUTSIDE any picker — handle before the ancestor check.
2469
2479
  // We assign IDs + the popover tag; x-dropdown owns popover mechanics.
2470
2480
  if (role === 'swatch') {
2471
- if (el._cpSwatchWired) return; // guard against re-firing via initTree
2472
- el._cpSwatchWired = true;
2473
-
2474
- // ---- Optional x-model binding ----
2475
- // x-model is the swatch's source of truth: reactive background preview,
2476
- // plus read (open) / write (close) accessors the picker uses.
2477
2481
  const modelExpr = el.getAttribute('x-model');
2478
2482
  if (modelExpr && window.Alpine?.evaluateLater && window.Alpine?.effect) {
2479
2483
  try {
2480
2484
  const readFn = Alpine.evaluateLater(el, modelExpr);
2481
2485
  // Reactive background preview
2482
- Alpine.effect(() => {
2486
+ const modelEffect = Alpine.effect(() => {
2483
2487
  readFn(v => {
2484
2488
  if (typeof v === 'string' && v.length) {
2485
2489
  el.style.setProperty('--color-picker-swatch', v);
@@ -2487,6 +2491,7 @@ function initializeColorpickerPlugin() {
2487
2491
  }
2488
2492
  });
2489
2493
  });
2494
+ cleanup(() => Alpine.release(modelEffect));
2490
2495
  el._cpModelGetter = (cb) => readFn(cb);
2491
2496
  // Writer: `<modelExpr> = <JSON value>` (color/gradient strings are JSON-safe).
2492
2497
  el._cpModelSetter = (v) => {
@@ -2495,6 +2500,12 @@ function initializeColorpickerPlugin() {
2495
2500
  } catch {}
2496
2501
  }
2497
2502
 
2503
+ if (el._cpSwatchWired) return; // guard against re-firing via initTree
2504
+ el._cpSwatchWired = true;
2505
+
2506
+ // ---- Optional x-model binding ----
2507
+ // x-model is the swatch's source of truth: reactive background preview,
2508
+ // plus read (open) / write (close) accessors the picker uses.
2498
2509
  // ---- Initial color via `value` attribute ----
2499
2510
  // Mirrors native <input type=color>: seeds the picker on first open and
2500
2511
  // the swatch CSS var so border-color paints before interaction.
@@ -165,7 +165,9 @@ function initializeComboboxPlugin() {
165
165
  Alpine.directive('combobox', (el, { modifiers, expression }, { cleanup }) => {
166
166
  captureModel(el); // fallback for any path that bypasses the above
167
167
  // Build after the current tick so sibling sources (datalist/menu) exist.
168
- setTimeout(() => build(el, modifiers, expression || '', cleanup), 0);
168
+ let removed = false;
169
+ cleanup(() => { removed = true; });
170
+ setTimeout(() => { if (!removed) build(el, modifiers, expression || '', cleanup); }, 0);
169
171
  });
170
172
 
171
173
  // Resolve the menu's owning top-layer context fresh, every time — never cache it.
@@ -201,6 +203,8 @@ function initializeComboboxPlugin() {
201
203
  // it into wherever the owner lives now. Harmless no-op otherwise.
202
204
  const m = el.__mnfstComboboxMenu;
203
205
  if (m) reattach(el, m);
206
+ // A re-initialized field: its cleanup released these
207
+ el.__cbBindEffects?.(cleanup);
204
208
  return;
205
209
  }
206
210
  el.__mnfstCombobox = true;
@@ -941,6 +945,8 @@ function initializeComboboxPlugin() {
941
945
  disabledObserver.observe(el, { attributes: true, attributeFilter: ['disabled'] });
942
946
  if (cleanup) cleanup(() => disabledObserver.disconnect());
943
947
 
948
+ const effectFactories = [];
949
+
944
950
  // ----- Reactive model (x-model) -----
945
951
  // Render chips from the bound value on init and external change; write back on
946
952
  // add/remove. Read inside an Alpine effect so $x / nested state stay reactive.
@@ -951,7 +957,7 @@ function initializeComboboxPlugin() {
951
957
  : (modelArrayShape === false ? vals.join(',') : vals.slice());
952
958
  try { Alpine.evaluate(el, `${modelExpr} = ${JSON.stringify(out)}`); } catch (_) { }
953
959
  };
954
- Alpine.effect(() => {
960
+ effectFactories.push(() => Alpine.effect(() => {
955
961
  read(raw => {
956
962
  if (modelArrayShape === null && raw != null && raw !== '') modelArrayShape = Array.isArray(raw);
957
963
  const incoming = modelToValues(raw);
@@ -960,20 +966,26 @@ function initializeComboboxPlugin() {
960
966
  if (!multiple && selected.length > 1) selected = selected.slice(-1);
961
967
  render();
962
968
  });
963
- });
969
+ }));
964
970
  }
965
971
 
966
972
  // ----- Reactive locked list (config-object form, so `locked` can gate on state) -----
967
973
  if (expr.startsWith('{')) {
968
- Alpine.effect(() => {
974
+ effectFactories.push(() => Alpine.effect(() => {
969
975
  let c; try { c = Alpine.evaluate(el, expr); } catch (_) { return; }
970
976
  const next = computeLocked(c && typeof c === 'object' && !Array.isArray(c) ? c : null);
971
977
  if (next.size === lockedSet.size && [...next].every(v => lockedSet.has(v))) return;
972
978
  lockedSet = next;
973
979
  render();
974
- });
980
+ }));
975
981
  }
976
982
 
983
+ el.__cbBindEffects = (onCleanup) => {
984
+ const effects = effectFactories.map((make) => make());
985
+ if (onCleanup) onCleanup(() => effects.forEach((fx) => Alpine.release(fx)));
986
+ };
987
+ el.__cbBindEffects(cleanup);
988
+
977
989
  // ----- Dynamic options: re-read when x-for / $x fills the source after build,
978
990
  // keeping the menu, chip labels, and data-locked flags current. -----
979
991
  if (src) {
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.261';
5
+ const MANIFEST_BUILD_VERSION = '0.5.262';
6
6
 
7
7
  /* Manifest Data Sources - Configuration */
8
8
 
@@ -108,6 +108,7 @@
108
108
 
109
109
  // Reactive registry id -> api, so $date('id') resolves across mount order.
110
110
  const _registry = Alpine.reactive ? Alpine.reactive({}) : {};
111
+ const _registryOwners = new Map();
111
112
  let _uid = 0;
112
113
 
113
114
  // Default English UI chrome; overridable via a data source's `_ui.date`.
@@ -173,6 +174,7 @@
173
174
  const now = new Date();
174
175
  const state = {
175
176
  rootEl,
177
+ _effects: [],
176
178
  id: rootEl.id || ('mnfst-dp-' + (++_uid)),
177
179
  selectionMode: 'single', // 'single' | 'range' | 'multiple'
178
180
  selected: null, // Date | null (single)
@@ -975,7 +977,7 @@
975
977
  }
976
978
  function bindConfigValue(state, el, expression) {
977
979
  if (!expression) return;
978
- Alpine.effect(() => {
980
+ state._effects.push(Alpine.effect(() => {
979
981
  let cfg;
980
982
  try { cfg = Alpine.evaluate(el, expression); } catch (_) { return; }
981
983
  if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) return;
@@ -990,14 +992,14 @@
990
992
  if (Array.isArray(cfg.presets)) { state.presets = cfg.presets; dirty = true; }
991
993
  if (cfg.format !== undefined) { state.format = cfg.format || null; dirty = true; }
992
994
  if (dirty) { state.bump(); if (state._mounted) state.render(); }
993
- });
995
+ }));
994
996
  }
995
997
  /* ---- Reactive chrome — re-render when locale, direction, or resolved `_ui`
996
998
  * text changes. Output comparison skips no-op _dataVersion bumps. */
997
999
  function bindReactiveRender(state) {
998
1000
  if (state._uiEffectBound) return;
999
1001
  state._uiEffectBound = true;
1000
- Alpine.effect(() => {
1002
+ state._effects.push(Alpine.effect(() => {
1001
1003
  let locale = '', dir = '';
1002
1004
  try { const ls = Alpine.store('locale'); locale = ls?.current || document.documentElement.lang || ''; dir = ls?.direction || ''; } catch (_) { }
1003
1005
  try { const ds = Alpine.store('data'); if (ds) void ds._dataVersion; } catch (_) { }
@@ -1008,7 +1010,7 @@
1008
1010
  state._uiKey = key;
1009
1011
  if (state._destroyed || first) return; // initial render happens at mount
1010
1012
  if (state._mounted) { state.render(); state.syncOut(); }
1011
- });
1013
+ }));
1012
1014
  }
1013
1015
 
1014
1016
  function bindModel(state, el, modelExpr) {
@@ -1016,7 +1018,23 @@
1016
1018
  state.modelGet = (cb) => { let out; read(v => { out = v; if (cb) cb(v); }); return out; };
1017
1019
  state.modelSet = (v) => { try { Alpine.evaluate(el, `${modelExpr} = ${JSON.stringify(v)}`); } catch (_) { } };
1018
1020
  // React to external model changes; syncOut writes the same value back (no loop).
1019
- Alpine.effect(() => { read(v => { const iso = (v instanceof Date) ? D.toISO(v) : v; const cur = state._primaryValue(); if (typeof iso === 'string' && iso !== cur) { const tm = iso.match(/T(\d{2}:\d{2})/); if (tm) state.time = tm[1]; const d = D.fromISO(iso); state.selected = d; if (d) state.view = { y: d.getFullYear(), m: d.getMonth() + 1 }; state.bump(); if (state._mounted) { state.render(); state.syncOut(); } } }); });
1021
+ state._effects.push(Alpine.effect(() => { read(v => { const iso = (v instanceof Date) ? D.toISO(v) : v; const cur = state._primaryValue(); if (typeof iso === 'string' && iso !== cur) { const tm = iso.match(/T(\d{2}:\d{2})/); if (tm) state.time = tm[1]; const d = D.fromISO(iso); state.selected = d; if (d) state.view = { y: d.getFullYear(), m: d.getMonth() + 1 }; state.bump(); if (state._mounted) { state.render(); state.syncOut(); } } }); }));
1022
+ }
1023
+
1024
+ // Global effects stay subscribed to the stores (and pin the calendar) until released.
1025
+ function register(id, state) {
1026
+ _registryOwners.set(id, state);
1027
+ _registry[id] = state.api;
1028
+ }
1029
+ function unregister(id, state) {
1030
+ if (!id || _registryOwners.get(id) !== state) return;
1031
+ _registryOwners.delete(id);
1032
+ delete _registry[id];
1033
+ }
1034
+ function disposeState(state) {
1035
+ state._destroyed = true;
1036
+ state._effects.forEach((fx) => Alpine.release(fx));
1037
+ state._effects.length = 0;
1020
1038
  }
1021
1039
 
1022
1040
  function createDefaultMenu(fieldEl) {
@@ -1075,9 +1093,9 @@
1075
1093
  if (modelExpr) bindModel(state, fieldEl, modelExpr);
1076
1094
 
1077
1095
  if (!state._mounted) { state.seed(); state.syncOut(); state.render(); state._mounted = true; }
1078
- if (calendar.id) _registry[calendar.id] = state.api;
1096
+ if (calendar.id) register(calendar.id, state);
1079
1097
  // Also resolve $date(fieldId) when the calendar is an auto-generated menu.
1080
- if (fieldEl.id) _registry[fieldEl.id] = state.api;
1098
+ if (fieldEl.id) register(fieldEl.id, state);
1081
1099
 
1082
1100
  // CSS anchor positioning (as in the dropdowns plugin); showPopover() on click
1083
1101
  // since a text <input> can't carry popovertarget.
@@ -1098,7 +1116,9 @@
1098
1116
  if (fieldEl._dpSynthesizedHidden && fieldEl._dpSynthesizedHidden.isConnected) fieldEl._dpSynthesizedHidden.remove();
1099
1117
  if (state._timeMenuEl && state._timeMenuEl.isConnected) state._timeMenuEl.remove();
1100
1118
  if (calendar && calendar.isConnected) calendar.remove(); // the generated dropdown
1101
- state._destroyed = true;
1119
+ unregister(fieldEl.id, state);
1120
+ unregister(calendar.id, state);
1121
+ disposeState(state);
1102
1122
  });
1103
1123
  }
1104
1124
 
@@ -1127,18 +1147,18 @@
1127
1147
 
1128
1148
  // Defer mount so linked fields can register first.
1129
1149
  setTimeout(() => {
1130
- if (state._mounted) return;
1150
+ if (state._mounted || state._destroyed) return;
1131
1151
  state.seed();
1132
1152
  state.render();
1133
1153
  state._mounted = true;
1134
- if (el.id) _registry[el.id] = state.api;
1154
+ if (el.id) register(el.id, state);
1135
1155
  bindReactiveRender(state);
1136
1156
  }, 0);
1137
1157
 
1138
1158
  cleanup(() => {
1139
- if (el.id) delete _registry[el.id];
1159
+ unregister(el.id, state);
1140
1160
  if (state._timeMenuEl && state._timeMenuEl.isConnected) state._timeMenuEl.remove();
1141
- state._destroyed = true;
1161
+ disposeState(state);
1142
1162
  delete el._dateState;
1143
1163
  });
1144
1164
  });
@@ -134,18 +134,41 @@ function register(el, rule, opts) {
134
134
  cleanup: []
135
135
  };
136
136
  wire(rec);
137
+ Alpine.onElRemoved?.(el, () => forget(rec));
137
138
  if (rule === 'route') routes.add(rec);
138
- else if (!rec.lazy) { addPending(rec); if (!el.closest('[x-route][hidden]')) track(rec); schedule(); }
139
+ else if (!rec.lazy) { addPending(rec); if (!el.closest('[x-route][hidden]')) track(rec); if (rec.urgent || warm.size < PREWARM_CAP) schedule(); }
139
140
  return rec;
140
141
  }
141
142
 
143
+ // A copy that renders like the original: a baked x-if clone is adopted through expandos
144
+ // cloneNode drops, so the copy leaves it out and its x-if renders fresh.
145
+ function pristineCopy(fragment) {
146
+ const copy = fragment.cloneNode(true);
147
+ const copies = copy.querySelectorAll('template[x-if]');
148
+ fragment.querySelectorAll('template[x-if]').forEach((t, i) => {
149
+ if (t._x_currentIfEl && t._x_currentIfEl === t.nextElementSibling) copies[i].nextElementSibling?.remove();
150
+ });
151
+ return copy;
152
+ }
153
+
142
154
  function render(rec, viaPrewarm) {
143
- if (rec.rendered) { if (!viaPrewarm && warm.delete(rec)) schedule(); return; }
155
+ if (rec.rendered) {
156
+ if (!viaPrewarm && warm.delete(rec)) {
157
+ rec.pristine = null; // opened: never evicted
158
+ schedule();
159
+ }
160
+ return;
161
+ }
144
162
  rec.rendered = true;
145
163
  pending.delete(rec);
146
164
  if (viaPrewarm) warm.add(rec); else { warm.delete(rec); untrack(rec); }
147
165
  const { el, tpl } = rec;
148
- const content = rec.discard ? tpl.content.cloneNode(true) : tpl.content;
166
+ // A prewarm can be evicted, and eviction must restore unused markup: re-initializing
167
+ // used nodes stacks listeners on any directive without cleanup and re-runs plugins that
168
+ // rewrite their markup. So it keeps a copy, and the originals (with whatever adoption
169
+ // set on them) still render.
170
+ if (viaPrewarm && !rec.discard) rec.pristine = pristineCopy(tpl.content);
171
+ const content = rec.discard ? pristineCopy(tpl.content) : tpl.content;
149
172
  Alpine.mutateDom(() => {
150
173
  tpl.remove();
151
174
  el.appendChild(content);
@@ -165,6 +188,30 @@ function teardown(rec) {
165
188
  });
166
189
  }
167
190
 
191
+ // Records hold the element and its anchor: a removed one must not pin its page.
192
+ function forget(rec) {
193
+ rec.forgotten = true;
194
+ rec.pristine = null;
195
+ pending.delete(rec);
196
+ routes.delete(rec);
197
+ pool = null;
198
+ untrack(rec);
199
+ rec.invoker = null;
200
+ if (warm.delete(rec)) schedule();
201
+ }
202
+
203
+ // A forgotten container initialized again (re-attached, or destroyed and re-walked).
204
+ // destroyTree also dropped its attribute watchers.
205
+ function revive(rec) {
206
+ if (!rec.forgotten) return;
207
+ rec.forgotten = false;
208
+ Alpine.onElRemoved?.(rec.el, () => forget(rec));
209
+ if (rec.rendered) return;
210
+ rec.rearm?.();
211
+ if (rec.rule === 'route') routes.add(rec);
212
+ else if (!rec.lazy) { addPending(rec); track(rec); schedule(); }
213
+ }
214
+
168
215
  // Evict a warm, still-closed, never-opened container back to its stash (non-urgent, least reachable first)
169
216
  function restash(rec) {
170
217
  if (!rec.rendered) return;
@@ -174,7 +221,8 @@ function restash(rec) {
174
221
  const { el, tpl } = rec;
175
222
  Alpine.mutateDom(() => {
176
223
  Array.from(el.children).forEach((child) => Alpine.destroyTree(child));
177
- while (el.firstChild) tpl.content.appendChild(el.firstChild);
224
+ while (el.firstChild) el.firstChild.remove();
225
+ if (rec.pristine) { tpl.content.appendChild(rec.pristine); rec.pristine = null; }
178
226
  el.appendChild(tpl);
179
227
  });
180
228
  warm.delete(rec);
@@ -226,6 +274,7 @@ function wire(rec) {
226
274
  render(rec);
227
275
  if (rec.discard) arm();
228
276
  });
277
+ rec.rearm = arm;
229
278
  arm();
230
279
  if (rec.discard) observe(['hidden'], () => { if (el.hasAttribute('hidden')) teardown(rec); });
231
280
  } else if (rule === 'route') {
@@ -235,6 +284,7 @@ function wire(rec) {
235
284
  if (!el.hasAttribute('hidden')) render(rec);
236
285
  if (!rec.rendered || rec.discard) arm();
237
286
  });
287
+ rec.rearm = arm;
238
288
  arm();
239
289
  if (rec.discard) observe(['hidden'], () => { if (el.hasAttribute('hidden')) teardown(rec); });
240
290
  } else if (rule === 'panel') {
@@ -300,6 +350,7 @@ function reachablePending() {
300
350
  if (pool && poolEpoch === reachEpoch && pool.length < pending.size * 2 + 64) return pool;
301
351
  poolEpoch = reachEpoch;
302
352
  pool = [];
353
+ for (const set of [warm, routes]) for (const rec of set) if (!rec.el.isConnected) { set.delete(rec); untrack(rec); }
303
354
  for (const rec of pending) {
304
355
  if (!rec.el.isConnected) { pending.delete(rec); untrack(rec); continue; }
305
356
  if (reachable(rec)) { track(rec); pool.push(rec); } else untrack(rec);
@@ -416,12 +467,14 @@ const inViewport = (r) => !!r && r.bottom > 0 && r.top < window.innerHeight && r
416
467
  // On-screen rect for ranking, or null when off-screen / boxless
417
468
  function nearRect(rec) {
418
469
  if (rec.box !== undefined) return rec.ioNear ? rec.box : null;
470
+ if (rec.anchor) return null; // IO reports within a frame; a read here forces layout per record
419
471
  const r = anchorRect(rec);
420
472
  rec.near = inViewport(r);
421
473
  return rec.near ? r : null;
422
474
  }
423
475
  function isNear(rec) {
424
476
  if (rec.box !== undefined) return !!rec.ioNear;
477
+ if (rec.anchor) return false;
425
478
  if (rec.near === undefined) rec.near = inViewport(anchorRect(rec));
426
479
  return rec.near;
427
480
  }
@@ -498,7 +551,7 @@ function defer(el) {
498
551
  }
499
552
 
500
553
  function intercept(el, skip) {
501
- if (el.__mnfstDefer) return;
554
+ if (el.__mnfstDefer) { revive(el.__mnfstDefer); return; }
502
555
  if (defer(el)) skip();
503
556
  }
504
557
 
@@ -11,7 +11,7 @@ let lastInputModality = 'mouse';
11
11
  // navigating item came from.
12
12
  let lastPointerTarget = null;
13
13
  document.addEventListener('keydown', () => { lastInputModality = 'keyboard'; }, true);
14
- document.addEventListener('pointerdown', (e) => { lastInputModality = 'mouse'; lastPointerTarget = e.target; }, true);
14
+ document.addEventListener('pointerdown', (e) => { lastInputModality = 'mouse'; lastPointerTarget = new WeakRef(e.target); }, true);
15
15
 
16
16
  // Context-menu dismiss (click outside / Escape). Module scope on purpose: a closure
17
17
  // made inside the directive shares its V8 context with every sibling closure, so it
@@ -755,7 +755,7 @@ document.addEventListener('click', (event) => {
755
755
  // the route change it dispatches instead. Hash links aren't intercepted and still
756
756
  // close via the normal path.
757
757
  window.addEventListener('manifest:route-change', () => {
758
- const pressed = lastPointerTarget;
758
+ const pressed = lastPointerTarget?.deref();
759
759
  document.querySelectorAll('[popover]:popover-open').forEach((menu) => {
760
760
  if (!menu.__mnfstCloseBound) return;
761
761
  const row = pressed && menu.contains(pressed) ? pressed : null;
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "manifest.appwrite.auth.js": "sha384-9WNhIeSEzxxJm+yP7cV41kV2xyzyXgFCNpThqiREcNbb6tnIW6O7VRZPL2jwy9Nk",
3
- "manifest.appwrite.data.js": "sha384-o/I02A1W20/hRbSQ4ihAlTVdMj03/v05BPIo24L93vCh1oDmJCy/mHtWXyO9P7z1",
3
+ "manifest.appwrite.data.js": "sha384-lNtw0QrcI1Hnshur38Dqw+iA9nTQ4GSnkeYUgKYdUWkGyqQ47XFiVAFr1LKGV/ZP",
4
4
  "manifest.appwrite.presences.js": "sha384-/CagPsgWhO1sWEevCNGJmZ/ezRK9YCjLjvF7hxNSM4gVlJmEVtfSLfF7wnsRq441",
5
5
  "manifest.bindings.js": "sha384-lPtMf8Ykaic9gNVZKq8Scia1881ZeMAyxer3WCIkUNwwrgzGBWto1OXADUwjvORF",
6
6
  "manifest.charts.js": "sha384-lzcoovnPCKhcwigrwYvc5rPAZuZNHwWmNNuu7hxPsfJuKAu8ejla5F7agHZ2Xft9",
7
7
  "manifest.chat.js": "sha384-88DfMfJLoi2oxclVD2iIMDkIGAExMXq5k47mekUredlicLaP3oZY5bwtwqs+TqDF",
8
8
  "manifest.code.js": "sha384-WGPlZE5Sp9GygUzJSmCdpLyYQTgvmEKc9BQS9Hmq1VTQwkqYJzPSWibHvPekcres",
9
9
  "manifest.color.js": "sha384-6Rv3LxyTcZNjrhtayQfqRdCx0uSZ4BiEbgEI98I62eTvp8Aw7LBIoNJ0Je1oktwL",
10
- "manifest.colorpicker.js": "sha384-BhcCe8cGcd0DTJNwzXWWY+FQNRfSFFl6XqfHIFP8yHg/aXFks1NIC6lVx0Sr1VkM",
11
- "manifest.combobox.js": "sha384-MILTkrgsTzsYUjGo4ZuADqV85Xs4GmfB4xexfe9vbJMIoQTZ42W4zs+DTaVSPEsx",
10
+ "manifest.colorpicker.js": "sha384-H6u6FHBbqIViQAYM9DTcFSVScghuvSEZqT1ZIKg7vv7zQx/5GiebDlJvyXdBxuM/",
11
+ "manifest.combobox.js": "sha384-eY5w3XHiFa1s6mXfmFbn/ff2GNHvlGUDAPVedlc0rvH4XhfBn3mWGLjj539q5IwS",
12
12
  "manifest.components.js": "sha384-BUFA8h+G5Plr82sV7bKzO42pPoiRZwRQcKj1lt2ROFiKjjlkayU/vmkAFqEsdBA+",
13
13
  "manifest.computed.js": "sha384-GYML/e81gQfN+iDbqhPUgny8bINCyL+SrbTofoGk4hlCnLLjv51mCv42JMqq+ku5",
14
- "manifest.data.js": "sha384-oOf9cFcoI3VFsfqg4eZyDVq7gdeF5DoIB92shYZI0N4ajWqhFx7B6Byy9H1g5HNE",
15
- "manifest.datepicker.js": "sha384-JeDdzdwXdXmnvgv1N1N+7WIQlezew9YJcvui3uVEvqAOX0WEaJO7ofLRRuc4hFBS",
16
- "manifest.defer.js": "sha384-GK9qJkYtchtyzSQ+3zmDQUJE3+au4qthT2M8JnFjXxgfwCH5IVKc2r3tackgMryU",
14
+ "manifest.data.js": "sha384-kwLHIxnThzjCDfjqJwq96lSb2dPcz4ntutRZ25jWmADaR9U8+T9ISU7GankNch4Z",
15
+ "manifest.datepicker.js": "sha384-tA4Dh8xXfyRw+3C19XXXDE0O6O6V6mGMdWv/lJA3Jcw/gcLG3aFTTEhvQEkJNkvb",
16
+ "manifest.defer.js": "sha384-5cHzFTbx+cZmGpk9FuiUtW3ciC9NvvHwJeHP7xL5yiZVp9PhMWVdQ7L9y6ju2yoo",
17
17
  "manifest.device.js": "sha384-YeQaS56L8IILqrQNBSeAOLwIh5DuAOrE60cYLK1+46qJtUrXlAvqGe2j5mbV4Vq7",
18
- "manifest.dropdowns.js": "sha384-H1IH7pU4zqjKNZtypXzwvUfnKkvNH96hM5za5xbb6SQlAucqkjKtlAepJW39nn/u",
18
+ "manifest.dropdowns.js": "sha384-TlFrYVWTXvpUmGnYaAk2til4lu0p6w5YcmWWEzZAokzd2qlmoyRLLXXVuYAK/AlR",
19
19
  "manifest.edit.js": "sha384-MOgQNdNLeHijhR0u2W6ntoOulKFglsATCFDqkinesOU15tHoILk/JNzHKkGi5dUG",
20
20
  "manifest.export.js": "sha384-Rx6XvqBZc4apzP/FwcDr6yIln4/zNa08Q4ZqBZcEqUz1aNZQfl4h0+Ae7mqCQyuG",
21
21
  "manifest.icons.js": "sha384-kKOINpX70YzNO3FvkV6umrmO3f0KIu0DNbwB+ej55a7y/lhU7v/myQtXL0bwCTbt",
@@ -28,11 +28,11 @@
28
28
  "manifest.slides.js": "sha384-3uRTkyK9XPLmnxI2+igZlpi4EyPlU/7IHj5j3BZJJ2KN455vXyk99fiXV3feO/XY",
29
29
  "manifest.status.js": "sha384-Zf07q/5tRS2F0o128sINYrce8xP035FTV0Ot7V7/hh+cwFhVZg+EWkHZN5b42aBn",
30
30
  "manifest.svg.js": "sha384-qr/8H9+2cIhSXNbRWRtIdrDlTOxKlNJqwEefHvCYoiBNW/kKhPTFa109Orv/EBnb",
31
- "manifest.sw.js": "sha384-Bm4NWZDrWtm0kekg7m9SnOve6wsB0nOjHzQre3TsvDaIlWK5vMlJAJ1qyV9cQGbi",
31
+ "manifest.sw.js": "sha384-my5z7X0SU63k0XDo1MuISuHz/R0iD8TOoJhwBs1hLSxAej366Skfo4QDtMl87EqJ",
32
32
  "manifest.tabs.js": "sha384-otU09H7jnawVKQdLAYpMCd3jx00tgyPVr42D2hLIBzN5dG4se505YesavdnMqEi1",
33
33
  "manifest.tailwind.js": "sha384-r/Jvqb+Piz21jUts10BbVAJoKkShOdJRVFpywyi4HPIi/OfOJq5CtPyWAPbdkMgt",
34
34
  "manifest.toasts.js": "sha384-3zkWxzw693pxg3QDFsoYCA3CqYaLKmBaMArULXODD66Utolpewl0OAeTB92aWUVv",
35
- "manifest.tooltips.js": "sha384-4b24PH5WsfFsewGea5SMWLwmrhC4kD6eDP/vvjNyjXYnQXXFGi4nLdojrIOwzG8M",
35
+ "manifest.tooltips.js": "sha384-hXGp5j0s6p2kGZY/hqLPEbaUv68ca63kFCjYiZqK3DcC12mpHTBy1vz3kuApyf4p",
36
36
  "manifest.url.parameters.js": "sha384-J2aSVBOPsoqsbafHndrWLHEDpKb4AKJAoQnVmKA4gZh5P/8HHjFWHO/lkusmG84l",
37
37
  "manifest.utilities.js": "sha384-h17EquaRtNcEW0yfhk7R+a+2+CzTlBxPuFryO6i9NV/OiIRMihREoyzQ77QawoQ7",
38
38
  "manifest.virtual.js": "sha384-ZDNoF9QVhVcVpkjIK6NH15S6I5c6OBshiJpXSOYjNsIB+298I0a7g3aFO+jyq4Vb",
@@ -16,7 +16,7 @@
16
16
 
17
17
  // Build stamps the published version here; unstamped (dev) falls back to the
18
18
  // registration URL's `v`, then 'dev'.
19
- const BUILD_VERSION = '0.5.261';
19
+ const BUILD_VERSION = '0.5.262';
20
20
  const CACHE_PREFIX = 'mnfst-sw:';
21
21
  const META_CACHE = CACHE_PREFIX + 'meta';
22
22
  // Sized to hold a full precache.json warm-up (PRECACHE_MAX_ENTRIES server-side)
@@ -171,6 +171,7 @@ function initializeTooltipPlugin() {
171
171
  s.el.style.setProperty('position-anchor', anchorName);
172
172
 
173
173
  s.activeTrigger = trigger;
174
+ trigger._tooltipSingleton = s;
174
175
  s.currentAnchorName = anchorName;
175
176
 
176
177
  // A11y: aria-describedby links trigger → tooltip for screen readers.
@@ -212,7 +213,20 @@ function initializeTooltipPlugin() {
212
213
 
213
214
  // ---- Directive ----
214
215
 
215
- Alpine.directive('tooltip', (el, { modifiers, expression }, { effect, evaluateLater }) => {
216
+ Alpine.directive('tooltip', (el, { modifiers, expression }, { effect, evaluateLater, cleanup }) => {
217
+ const listeners = new AbortController();
218
+ const { signal } = listeners;
219
+ cleanup(() => {
220
+ listeners.abort();
221
+ if (_pendingTrigger === el) cancelPendingShow();
222
+ // A removal root is already detached here, so its host can't be looked up
223
+ const s = el._tooltipSingleton;
224
+ el._tooltipSingleton = null;
225
+ if (s && s.activeTrigger === el) {
226
+ if (s.el.matches(':popover-open')) s.el.hidePopover();
227
+ s.activeTrigger = null;
228
+ }
229
+ });
216
230
 
217
231
  // --- Content ---
218
232
  // Escape by default; author literal HTML or .html/.safe opts in.
@@ -288,15 +302,15 @@ function initializeTooltipPlugin() {
288
302
  };
289
303
 
290
304
  // Mouse interactions
291
- el.addEventListener('mouseenter', requestShow);
292
- el.addEventListener('mouseleave', requestHide);
305
+ el.addEventListener('mouseenter', requestShow, { signal });
306
+ el.addEventListener('mouseleave', requestHide, { signal });
293
307
 
294
308
  // Keyboard focus (WCAG 2.1 SC 1.4.13). Gated on :focus-visible so mouse-click
295
309
  // focus doesn't flash the tooltip.
296
310
  el.addEventListener('focus', () => {
297
311
  if (el.matches(':focus-visible')) requestShow();
298
- });
299
- el.addEventListener('blur', requestHide);
312
+ }, { signal });
313
+ el.addEventListener('blur', requestHide, { signal });
300
314
 
301
315
  // Mousedown/click hides now and clears the chain window, so a synthetic
302
316
  // re-hover (content shifting under the cursor) waits the full delay.
@@ -305,8 +319,8 @@ function initializeTooltipPlugin() {
305
319
  hideAnySingleton();
306
320
  clearChainWindow();
307
321
  };
308
- el.addEventListener('mousedown', hideOnInteraction);
309
- el.addEventListener('click', hideOnInteraction);
322
+ el.addEventListener('mousedown', hideOnInteraction, { signal });
323
+ el.addEventListener('click', hideOnInteraction, { signal });
310
324
  });
311
325
 
312
326
  // Global: when ANY other popover opens, close the singleton(s). Dropdowns and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.261",
3
+ "version": "0.5.262",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",