mnfst 0.5.260 → 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.260';
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.260';
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
@@ -38,6 +38,114 @@ function ensureContextDismiss(menu, owner) {
38
38
  bindContextDismiss(menu, ctxAbort.signal);
39
39
  }
40
40
 
41
+ // A removed trigger stops being the menu's context host and dismiss owner. A menu it
42
+ // opened closes with it; one opened by another trigger keeps its dismiss listeners.
43
+ function releaseContextTrigger(menu, el) {
44
+ if (menu._triggerHost === el) {
45
+ menu._triggerHost = null;
46
+ menu._triggerEl = null;
47
+ hideIfOpen(menu);
48
+ }
49
+ if (menu.__mnfstCtxOwner === el) {
50
+ menu.__mnfstContextAbort?.abort();
51
+ menu.__mnfstContextDismissBound = false;
52
+ menu.__mnfstContextAbort = null;
53
+ menu.__mnfstCtxOwner = null;
54
+ const host = menu._triggerHost;
55
+ if (host && host.isConnected && menu.matches(':popover-open')) ensureContextDismiss(menu, host);
56
+ }
57
+ }
58
+
59
+ function hideIfOpen(menu) {
60
+ try { if (menu.matches(':popover-open')) menu.hidePopover(); } catch (_) { }
61
+ }
62
+
63
+ // Menus the plugin cloned into <body> (template, component) belong to the triggers
64
+ // using them, and leave with the last one. Removal waits a tick: a menu item's own
65
+ // click may be what removed the trigger, and its dispatch must finish in the DOM.
66
+ function retainManagedMenu(menu, el) {
67
+ (menu.__mnfstManagedBy ||= new Set()).add(el);
68
+ }
69
+
70
+ function releaseManagedMenu(menu, el) {
71
+ const users = menu.__mnfstManagedBy;
72
+ if (!users || !users.delete(el) || users.size) return;
73
+ hideIfOpen(menu);
74
+ setTimeout(() => {
75
+ if (users.size) return;
76
+ menu.__mnfstManagedBy = null;
77
+ window.Alpine?.destroyTree?.(menu);
78
+ menu.remove();
79
+ }, 0);
80
+ }
81
+
82
+ // Per-menu handlers (bound once, outliving any one trigger). Module scope for the same
83
+ // reason as bindContextDismiss: they must hold the menu and nothing else.
84
+ function bindMenuRoles(menu) {
85
+ const roleItems = () => menu.querySelectorAll('li').forEach((li) => {
86
+ if (!li.hasAttribute('role')) li.setAttribute('role', 'menuitem');
87
+ });
88
+ roleItems();
89
+ if (menu.__mnfstRoleOnRender) return;
90
+ menu.__mnfstRoleOnRender = true;
91
+ menu.addEventListener('manifest:defer-render', roleItems);
92
+ }
93
+
94
+ function bindMenuAriaToggle(menu) {
95
+ if (menu.__mnfstAriaToggleBound) return;
96
+ menu.__mnfstAriaToggleBound = true;
97
+ menu.addEventListener('toggle', (e) => {
98
+ const expanded = e.newState === 'open' ? 'true' : 'false';
99
+ document.querySelectorAll(`[aria-controls="${menu.id}"]`).forEach(t => t.setAttribute('aria-expanded', expanded));
100
+ });
101
+ }
102
+
103
+ const MENU_ROW = 'li, a, button, label, [role=menuitem], [role=option]';
104
+ const MENU_CONTROL = 'input, select, textarea, [contenteditable]:not([contenteditable=false]), [role=switch], .combobox, [x-dropdown], [popovertarget], [keep-open]';
105
+
106
+ function bindMenuClose(menu) {
107
+ if (menu.__mnfstCloseBound) return;
108
+ menu.__mnfstCloseBound = true;
109
+
110
+ // Close the item's menu plus any menus it was nested in. The owned
111
+ // popover isn't always a <menu> (e.g. .dropdown-menu on a div), so
112
+ // fall back to it rather than relying on the ancestor lookup.
113
+ const closeFrom = (node) => {
114
+ let m = node.closest('menu[popover]') || menu;
115
+ while (m) {
116
+ if (m.matches(':popover-open')) m.hidePopover();
117
+ m = m.parentElement?.closest('menu[popover]');
118
+ }
119
+ if (lastInputModality === 'keyboard') {
120
+ const trigger = menu.__mnfstTrigger || menu._triggerHost || document.querySelector(`[aria-controls="${menu.id}"]`);
121
+ trigger?.focus?.();
122
+ }
123
+ };
124
+
125
+ menu.addEventListener('click', (e) => {
126
+ // Explicit opt-in wins over every exclusion below
127
+ const opted = e.target.closest('[close]');
128
+ if (opted && menu.contains(opted)) return closeFrom(opted);
129
+ if (!menu.__mnfstAutoClose) return;
130
+
131
+ // Outermost row between the click target and the menu
132
+ let row = null;
133
+ for (let n = e.target; n && n !== menu; n = n.parentElement) {
134
+ if (n.matches?.(MENU_ROW)) row = n;
135
+ }
136
+ if (!row) return;
137
+
138
+ // A control that *is* the row is a menu item; one nested
139
+ // inside it is embedded UI, so leave the menu open
140
+ const hit = e.target.closest(MENU_CONTROL);
141
+ if (hit && hit !== row && row.contains(hit)) return;
142
+ if (row.querySelector(MENU_CONTROL)) return;
143
+ if (row.matches('[role=switch], [x-dropdown], [popovertarget], [keep-open]')) return;
144
+
145
+ closeFrom(row);
146
+ });
147
+ }
148
+
41
149
  function initializeDropdownPlugin() {
42
150
  // Ensure an Alpine context exists. Keep the scope empty — seeding properties
43
151
  // collides with author state and the tabs plugin's `tab` property.
@@ -79,6 +187,11 @@ function initializeDropdownPlugin() {
79
187
  registerDirective('dropdown', (el, { modifiers, expression }, { effect, evaluateLater, cleanup }) => {
80
188
  let menu;
81
189
 
190
+ // Shared hover state for all dropdown types
191
+ let hoverTimeout;
192
+ let autoCloseTimeout;
193
+ let startAutoCloseTimer;
194
+
82
195
  // Listeners this trigger adds to things that outlive it (its menu, document)
83
196
  // are bound with this run's signal and aborted when Alpine tears the trigger
84
197
  // down, or when the effect re-runs setup. Without it every re-rendered trigger
@@ -88,23 +201,17 @@ function initializeDropdownPlugin() {
88
201
  cleanup(() => {
89
202
  if (runAbort) runAbort.abort();
90
203
  runAbort = null;
91
- if (menu && menu.__mnfstCtxOwner === el) {
92
- menu.__mnfstContextAbort?.abort();
93
- menu.__mnfstContextDismissBound = false;
94
- menu.__mnfstContextAbort = null;
95
- menu.__mnfstCtxOwner = null;
96
- }
97
- if (menu && menu.__mnfstTrigger === el) menu.__mnfstTrigger = null;
98
- if (menu && menu._triggerHost === el) { menu._triggerHost = null; menu._triggerEl = null; }
204
+ clearTimeout(hoverTimeout);
205
+ clearTimeout(autoCloseTimeout);
206
+ if (!menu) return;
207
+ releaseContextTrigger(menu, el);
208
+ if (menu.__mnfstTrigger === el) menu.__mnfstTrigger = null;
209
+ releaseManagedMenu(menu, el);
99
210
  });
100
211
 
101
- // Shared hover state for all dropdown types
102
- let hoverTimeout;
103
- let autoCloseTimeout;
104
- let startAutoCloseTimer;
105
-
106
212
  effect(() => {
107
213
  if (runAbort) runAbort.abort();
214
+ if (menu) releaseManagedMenu(menu, el);
108
215
  const runCtl = new AbortController();
109
216
  runAbort = runCtl;
110
217
  const signal = runCtl.signal;
@@ -146,6 +253,7 @@ function initializeDropdownPlugin() {
146
253
  const uniqueDropdownId = `dropdown-${anchorCode}`;
147
254
  menu.setAttribute('id', uniqueDropdownId);
148
255
  document.body.appendChild(menu);
256
+ retainManagedMenu(menu, el);
149
257
  // Appending to <body> loses the fact that the trigger sits inside a
150
258
  // dialog, which is how manifest.dialog.css tells a dialog's own menus
151
259
  // from the page's. An authored menu keeps its place and needs no mark.
@@ -171,6 +279,16 @@ function initializeDropdownPlugin() {
171
279
  const waitForComponent = async () => {
172
280
  const loader = window.ManifestComponentsLoader;
173
281
  const content = await loader.loadComponent(componentName);
282
+ if (signal.aborted) return;
283
+ // Another trigger of this menu may have cloned it during the await
284
+ const loaded = document.getElementById(dropdownId);
285
+ if (loaded) {
286
+ menu = loaded;
287
+ if (menu.__mnfstManagedBy) retainManagedMenu(menu, el);
288
+ if (!modifiers.includes('context')) el.setAttribute('popovertarget', dropdownId);
289
+ setupDropdown();
290
+ return;
291
+ }
174
292
  if (content) {
175
293
  // Create a temporary container to parse the component
176
294
  const tempDiv = document.createElement('div');
@@ -182,6 +300,7 @@ function initializeDropdownPlugin() {
182
300
  menu = menuElement.cloneNode(true);
183
301
  menu.setAttribute('id', dropdownId);
184
302
  document.body.appendChild(menu);
303
+ retainManagedMenu(menu, el);
185
304
  if (el.closest('[popover]')) menu.setAttribute('data-popover-nested', '');
186
305
  if (!modifiers.includes('context')) el.setAttribute('popovertarget', dropdownId);
187
306
 
@@ -216,6 +335,7 @@ function initializeDropdownPlugin() {
216
335
  console.warn(`[Manifest] Dropdown menu with id "${dropdownId}" not found`);
217
336
  return;
218
337
  }
338
+ if (menu.__mnfstManagedBy) retainManagedMenu(menu, el);
219
339
  if (!modifiers.includes('context')) el.setAttribute('popovertarget', dropdownId);
220
340
  }
221
341
 
@@ -241,10 +361,9 @@ function initializeDropdownPlugin() {
241
361
  const a = el.style.getPropertyValue('--trigger-anchor');
242
362
  if (a) menu.style.setProperty('position-anchor', a);
243
363
  };
244
- if (!modifiers.includes('context') && !el.__mnfstAnchorSwapBound) {
245
- el.__mnfstAnchorSwapBound = true;
246
- el.addEventListener('pointerdown', pointMenuHere);
247
- el.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') pointMenuHere(); });
364
+ if (!modifiers.includes('context')) {
365
+ el.addEventListener('pointerdown', pointMenuHere, { signal });
366
+ el.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') pointMenuHere(); }, { signal });
248
367
  }
249
368
 
250
369
  // A11y wiring (WAI-ARIA Menu Button): trigger gets aria-haspopup/
@@ -258,22 +377,9 @@ function initializeDropdownPlugin() {
258
377
  if (!menu.hasAttribute('role')) menu.setAttribute('role', 'menu');
259
378
  // Deferred menus get their items later — role them off the gesture,
260
379
  // never in the toggle task (243 role writes on a visible list ≈ 70ms)
261
- const roleItems = () => menu.querySelectorAll('li').forEach((li) => {
262
- if (!li.hasAttribute('role')) li.setAttribute('role', 'menuitem');
263
- });
264
- roleItems();
265
- if (!menu.__mnfstRoleOnRender) {
266
- menu.__mnfstRoleOnRender = true;
267
- menu.addEventListener('manifest:defer-render', roleItems);
268
- }
380
+ bindMenuRoles(menu);
269
381
  // Keep aria-expanded in sync across every trigger of this menu.
270
- if (!menu.__mnfstAriaToggleBound) {
271
- menu.__mnfstAriaToggleBound = true;
272
- menu.addEventListener('toggle', (e) => {
273
- const expanded = e.newState === 'open' ? 'true' : 'false';
274
- document.querySelectorAll(`[aria-controls="${menu.id}"]`).forEach(t => t.setAttribute('aria-expanded', expanded));
275
- });
276
- }
382
+ bindMenuAriaToggle(menu);
277
383
  }
278
384
 
279
385
  // Set up hover functionality after menu is ready
@@ -312,8 +418,8 @@ function initializeDropdownPlugin() {
312
418
  }, 300); // Small delay to prevent accidental closes
313
419
  };
314
420
 
315
- el.addEventListener('mouseenter', handleShowPopover);
316
- el.addEventListener('mouseleave', startAutoCloseTimer);
421
+ el.addEventListener('mouseenter', handleShowPopover, { signal });
422
+ el.addEventListener('mouseleave', startAutoCloseTimer, { signal });
317
423
  }
318
424
 
319
425
  // Set up context menu (right-click) functionality
@@ -359,84 +465,78 @@ function initializeDropdownPlugin() {
359
465
  });
360
466
  };
361
467
 
362
- // Bind once per element (reprocessing would stack duplicates).
363
- if (!el.__mnfstContextTriggerBound) {
364
- el.__mnfstContextTriggerBound = true;
365
- el.addEventListener('contextmenu', (e) => {
366
- e.preventDefault();
367
- e.stopPropagation();
468
+ el.addEventListener('contextmenu', (e) => {
469
+ e.preventDefault();
470
+ e.stopPropagation();
368
471
 
369
- // Android fires a real contextmenu after a touch long-press too —
370
- // our pointer path already opened the menu, so ignore the dupe.
371
- if (el.__mnfstLongPressOpenedAt && Date.now() - el.__mnfstLongPressOpenedAt < LONG_PRESS_DUPE_WINDOW_MS) return;
472
+ // Android fires a real contextmenu after a touch long-press too —
473
+ // our pointer path already opened the menu, so ignore the dupe.
474
+ if (el.__mnfstLongPressOpenedAt && Date.now() - el.__mnfstLongPressOpenedAt < LONG_PRESS_DUPE_WINDOW_MS) return;
372
475
 
373
- openContextMenuAt(e.target, e.clientX, e.clientY);
374
- });
476
+ openContextMenuAt(e.target, e.clientX, e.clientY);
477
+ }, { signal });
375
478
 
376
- // iOS Safari never fires `contextmenu` for a touch long-press (Android
377
- // does). Drive the same open from pointer events instead — touch/pen
378
- // only, mouse keeps native contextmenu semantics.
379
- const LONG_PRESS_MS = 500;
380
- const MOVE_SLOP_PX = 10;
381
- let pressTimer = null;
382
- let startX = 0, startY = 0;
383
- let pressTarget = null;
384
-
385
- const suppressCallout = () => {
386
- el.style.setProperty('-webkit-touch-callout', 'none');
387
- el.style.setProperty('-webkit-user-select', 'none');
388
- el.style.setProperty('user-select', 'none');
389
- };
390
- const restoreCallout = () => {
391
- el.style.removeProperty('-webkit-touch-callout');
392
- el.style.removeProperty('-webkit-user-select');
393
- el.style.removeProperty('user-select');
394
- };
395
- const cancelPress = () => {
396
- if (pressTimer) clearTimeout(pressTimer);
479
+ // iOS Safari never fires `contextmenu` for a touch long-press (Android
480
+ // does). Drive the same open from pointer events instead — touch/pen
481
+ // only, mouse keeps native contextmenu semantics.
482
+ const LONG_PRESS_MS = 500;
483
+ const MOVE_SLOP_PX = 10;
484
+ let pressTimer = null;
485
+ let startX = 0, startY = 0;
486
+ let pressTarget = null;
487
+
488
+ const suppressCallout = () => {
489
+ el.style.setProperty('-webkit-touch-callout', 'none');
490
+ el.style.setProperty('-webkit-user-select', 'none');
491
+ el.style.setProperty('user-select', 'none');
492
+ };
493
+ const restoreCallout = () => {
494
+ el.style.removeProperty('-webkit-touch-callout');
495
+ el.style.removeProperty('-webkit-user-select');
496
+ el.style.removeProperty('user-select');
497
+ };
498
+ const cancelPress = () => {
499
+ if (pressTimer) clearTimeout(pressTimer);
500
+ pressTimer = null;
501
+ restoreCallout();
502
+ };
503
+
504
+ el.addEventListener('pointerdown', (e) => {
505
+ if (e.pointerType !== 'touch' && e.pointerType !== 'pen') return;
506
+ cancelPress();
507
+ startX = e.clientX;
508
+ startY = e.clientY;
509
+ pressTarget = e.target;
510
+ suppressCallout();
511
+ pressTimer = setTimeout(() => {
397
512
  pressTimer = null;
513
+ if (signal.aborted) return;
398
514
  restoreCallout();
399
- };
400
-
401
- el.addEventListener('pointerdown', (e) => {
402
- if (e.pointerType !== 'touch' && e.pointerType !== 'pen') return;
403
- cancelPress();
404
- startX = e.clientX;
405
- startY = e.clientY;
406
- pressTarget = e.target;
407
- suppressCallout();
408
- pressTimer = setTimeout(() => {
409
- pressTimer = null;
410
- restoreCallout();
411
- el.__mnfstLongPressOpenedAt = Date.now();
412
- // No contextmenu event on iOS: announce the press so composing plugins (x-edit) can track the target.
413
- pressTarget.dispatchEvent(new CustomEvent('dropdown:context', { bubbles: true, detail: { x: startX, y: startY } }));
414
- openContextMenuAt(pressTarget, startX, startY);
415
- }, LONG_PRESS_MS);
416
- });
417
- el.addEventListener('pointermove', (e) => {
418
- if (!pressTimer) return;
419
- if (Math.abs(e.clientX - startX) > MOVE_SLOP_PX || Math.abs(e.clientY - startY) > MOVE_SLOP_PX) cancelPress();
420
- });
421
- el.addEventListener('pointerup', cancelPress);
422
- el.addEventListener('pointercancel', cancelPress);
423
- el.addEventListener('scroll', cancelPress, true);
424
-
425
- // Suppress the synthetic click that follows the touch sequence which
426
- // opened the menu, so it doesn't also activate/navigate the trigger.
427
- el.addEventListener('click', (e) => {
428
- if (el.__mnfstLongPressOpenedAt && Date.now() - el.__mnfstLongPressOpenedAt < LONG_PRESS_DUPE_WINDOW_MS) {
429
- e.preventDefault();
430
- e.stopPropagation();
431
- }
432
- }, true);
433
- }
515
+ el.__mnfstLongPressOpenedAt = Date.now();
516
+ // No contextmenu event on iOS: announce the press so composing plugins (x-edit) can track the target.
517
+ pressTarget.dispatchEvent(new CustomEvent('dropdown:context', { bubbles: true, detail: { x: startX, y: startY } }));
518
+ openContextMenuAt(pressTarget, startX, startY);
519
+ }, LONG_PRESS_MS);
520
+ }, { signal });
521
+ el.addEventListener('pointermove', (e) => {
522
+ if (!pressTimer) return;
523
+ if (Math.abs(e.clientX - startX) > MOVE_SLOP_PX || Math.abs(e.clientY - startY) > MOVE_SLOP_PX) cancelPress();
524
+ }, { signal });
525
+ el.addEventListener('pointerup', cancelPress, { signal });
526
+ el.addEventListener('pointercancel', cancelPress, { signal });
527
+ el.addEventListener('scroll', cancelPress, { capture: true, signal });
528
+
529
+ // Suppress the synthetic click that follows the touch sequence which
530
+ // opened the menu, so it doesn't also activate/navigate the trigger.
531
+ el.addEventListener('click', (e) => {
532
+ if (el.__mnfstLongPressOpenedAt && Date.now() - el.__mnfstLongPressOpenedAt < LONG_PRESS_DUPE_WINDOW_MS) {
533
+ e.preventDefault();
534
+ e.stopPropagation();
535
+ }
536
+ }, { capture: true, signal });
434
537
 
435
- // Document-level dismiss listeners are global — bind once per
436
- // menu via an AbortController so reprocessing can't leak them.
437
- // Bound once per menu, owned by one trigger and released in that
438
- // trigger's cleanup (see top). Any other trigger of the same menu
439
- // re-binds on its next open, so a shared menu never loses dismiss.
538
+ // Owned by one trigger, released in its cleanup. Any other trigger of
539
+ // the same menu re-binds on its next open.
440
540
  ensureContextDismiss(menu, el);
441
541
  }
442
542
 
@@ -445,47 +545,7 @@ function initializeDropdownPlugin() {
445
545
  // Context menus auto-close by nature, so .close is implied.
446
546
  if (modifiers.includes('close') || modifiers.includes('context')) menu.__mnfstAutoClose = true;
447
547
 
448
- if (!menu.__mnfstCloseBound) {
449
- menu.__mnfstCloseBound = true;
450
-
451
- const ROW = 'li, a, button, label, [role=menuitem], [role=option]';
452
- const CONTROL = 'input, select, textarea, [contenteditable]:not([contenteditable=false]), [role=switch], .combobox, [x-dropdown], [popovertarget], [keep-open]';
453
-
454
- // Close the item's menu plus any menus it was nested in. The owned
455
- // popover isn't always a <menu> (e.g. .dropdown-menu on a div), so
456
- // fall back to it rather than relying on the ancestor lookup.
457
- const closeFrom = (node) => {
458
- let m = node.closest('menu[popover]') || menu;
459
- while (m) {
460
- if (m.matches(':popover-open')) m.hidePopover();
461
- m = m.parentElement?.closest('menu[popover]');
462
- }
463
- if (lastInputModality === 'keyboard') (menu.__mnfstTrigger || el).focus?.();
464
- };
465
-
466
- menu.addEventListener('click', (e) => {
467
- // Explicit opt-in wins over every exclusion below
468
- const opted = e.target.closest('[close]');
469
- if (opted && menu.contains(opted)) return closeFrom(opted);
470
- if (!menu.__mnfstAutoClose) return;
471
-
472
- // Outermost row between the click target and the menu
473
- let row = null;
474
- for (let n = e.target; n && n !== menu; n = n.parentElement) {
475
- if (n.matches?.(ROW)) row = n;
476
- }
477
- if (!row) return;
478
-
479
- // A control that *is* the row is a menu item; one nested
480
- // inside it is embedded UI, so leave the menu open
481
- const hit = e.target.closest(CONTROL);
482
- if (hit && hit !== row && row.contains(hit)) return;
483
- if (row.querySelector(CONTROL)) return;
484
- if (row.matches('[role=switch], [x-dropdown], [popovertarget], [keep-open]')) return;
485
-
486
- closeFrom(row);
487
- });
488
- }
548
+ bindMenuClose(menu);
489
549
 
490
550
  // Add keyboard navigation handling
491
551
  menu.addEventListener('keydown', (e) => {
@@ -695,7 +755,7 @@ document.addEventListener('click', (event) => {
695
755
  // the route change it dispatches instead. Hash links aren't intercepted and still
696
756
  // close via the normal path.
697
757
  window.addEventListener('manifest:route-change', () => {
698
- const pressed = lastPointerTarget;
758
+ const pressed = lastPointerTarget?.deref();
699
759
  document.querySelectorAll('[popover]:popover-open').forEach((menu) => {
700
760
  if (!menu.__mnfstCloseBound) return;
701
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-NrUgzRg0XIrS/BYh6iw9V/S5MH2EkMy7935n7UnT+nq9nUrPtVBRHlmfNuymT9R4",
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-OXdlXbo1eKsq/47pj6A9qPTyxtvvjxycFzgZeE5tmLbIT/Nj+A1JdofNmoV9/iRa",
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-ORVhBVvqhx58sd7Ij8b+NOIkjWF2TGcz5a8BC+CIswsS8TDLtyIDqD3YM4HDsGzB",
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-V4isxMDJOOMSqedY9JNCeVXhe4tJTA/GUBReX9dghYtNr68cUhsqur76nOmlO3iF",
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.260';
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.260",
3
+ "version": "0.5.262",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",