mnfst 0.5.57 → 0.5.59

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.
@@ -1,360 +1,311 @@
1
- /* Manifest Tooltips */
1
+ /* Manifest Tooltips — singleton architecture.
2
+ *
3
+ * Instead of creating one <div popover="hint"> per x-tooltip trigger, this plugin
4
+ * maintains ONE tooltip element per popover host (usually just document.body plus
5
+ * optionally one per open popover). Every trigger with x-tooltip becomes a
6
+ * lightweight content provider that asks the shared controller to show its text,
7
+ * anchored to that trigger.
8
+ *
9
+ * Why: N triggers × 1 tooltip each = N extra DOM nodes that are empty 99% of the
10
+ * time. For dense UIs like colorpicker libraries (~300+ swatches × 20 pickers),
11
+ * this is the difference between a usable page and a laggy one.
12
+ */
2
13
 
3
- // Get tooltip hover delay from CSS variable
14
+ // Hover delay from CSS var (with time-unit parsing). Defaults to 500ms.
4
15
  function getTooltipHoverDelay(element) {
5
- // Try to get the value from the element first, then from document root
6
16
  let computedStyle = getComputedStyle(element);
7
17
  let delayValue = computedStyle.getPropertyValue('--tooltip-hover-delay').trim();
8
-
9
18
  if (!delayValue) {
10
- // If not found on element, check document root
11
19
  computedStyle = getComputedStyle(document.documentElement);
12
20
  delayValue = computedStyle.getPropertyValue('--tooltip-hover-delay').trim();
13
21
  }
14
-
15
- if (!delayValue) {
16
- return 500; // Default to 500ms if not set
17
- }
18
-
19
- // Parse CSS time value (supports various time units)
22
+ if (!delayValue) return 500;
20
23
  const timeValue = parseFloat(delayValue);
21
-
22
- if (delayValue.endsWith('s')) {
23
- return timeValue * 1000; // Convert seconds to milliseconds
24
- } else if (delayValue.endsWith('ms')) {
25
- return timeValue; // Already in milliseconds
26
- } else if (delayValue.endsWith('m')) {
27
- return timeValue * 60 * 1000; // Convert minutes to milliseconds
28
- } else if (delayValue.endsWith('h')) {
29
- return timeValue * 60 * 60 * 1000; // Convert hours to milliseconds
30
- } else if (delayValue.endsWith('min')) {
31
- return timeValue * 60 * 1000; // Convert minutes to milliseconds
32
- } else if (delayValue.endsWith('sec')) {
33
- return timeValue * 1000; // Convert seconds to milliseconds
34
- } else if (delayValue.endsWith('second')) {
35
- return timeValue * 1000; // Convert seconds to milliseconds
36
- } else if (delayValue.endsWith('minute')) {
37
- return timeValue * 60 * 1000; // Convert minutes to milliseconds
38
- } else if (delayValue.endsWith('hour')) {
39
- return timeValue * 60 * 60 * 1000; // Convert hours to milliseconds
40
- } else {
41
- // If no unit, assume milliseconds (backward compatibility)
42
- return timeValue;
43
- }
24
+ if (delayValue.endsWith('ms')) return timeValue;
25
+ if (delayValue.endsWith('s')) return timeValue * 1000;
26
+ if (delayValue.endsWith('min') || delayValue.endsWith('m') || delayValue.endsWith('minute')) return timeValue * 60 * 1000;
27
+ if (delayValue.endsWith('h') || delayValue.endsWith('hour')) return timeValue * 60 * 60 * 1000;
28
+ if (delayValue.endsWith('sec') || delayValue.endsWith('second')) return timeValue * 1000;
29
+ return timeValue; // unitless → ms
44
30
  }
45
31
 
46
- /** Keep anchor-name on the trigger long after hide so position-anchor stays valid through any close transition/layout. */
47
- const ANCHOR_RESTORE_DELAY_MS = 2000;
48
-
49
- /**
50
- * DOM parent for the tooltip popover. Must be the same popover subtree as the trigger when the trigger
51
- * lives inside menu/dialog/etc.; otherwise CSS anchor positioning cannot resolve (tooltip in body + anchor
52
- * inside top-layer popover → invalid position-anchor, jump to origin).
53
- */
32
+ // Popover host for anchor positioning: the closest top-layer popover ancestor, or body.
54
33
  function getTooltipHostForTrigger(triggerEl) {
55
34
  return triggerEl.closest('[popover]') || document.body;
56
35
  }
57
36
 
58
37
  function initializeTooltipPlugin() {
59
38
 
60
- Alpine.directive('tooltip', (el, { modifiers, expression }, { effect, evaluateLater }) => {
61
-
62
- let getTooltipContent;
63
-
64
- // If it starts with $x, handle content loading
65
- if (expression.startsWith('$x.')) {
66
- const path = expression.substring(3); // Remove '$x.'
67
- const [contentType, ...pathParts] = path.split('.');
68
-
69
- // Create evaluator that uses the $x magic method
70
- getTooltipContent = evaluateLater(expression);
39
+ // Chain mode: if another tooltip was dismissed this recently, the next one
40
+ // shows immediately (no hover delay). Also used to skip the hide-show flicker
41
+ // when gliding across many triggers — the singleton just re-anchors.
42
+ const TOOLTIP_CHAIN_GRACE_MS = 250;
43
+ let _lastTooltipHideTime = 0;
44
+ const markTooltipHidden = () => { _lastTooltipHideTime = Date.now(); };
45
+ const isInChainWindow = () => (Date.now() - _lastTooltipHideTime) < TOOLTIP_CHAIN_GRACE_MS;
46
+
47
+ // ---- Singletons per host ----
48
+ //
49
+ // Most pages only need one singleton (under document.body). Open popovers (menus,
50
+ // dialogs) require their own singleton because CSS anchor positioning can't resolve
51
+ // across the top-layer boundary. We create them lazily on first use and keep them
52
+ // (small, hidden <div>s) for the life of the host.
53
+ const _singletons = new WeakMap();
54
+
55
+ function getSingleton(host) {
56
+ let s = _singletons.get(host);
57
+ if (s) return s;
58
+ const el = document.createElement('div');
59
+ el.setAttribute('popover', 'hint');
60
+ el.className = 'tooltip';
61
+ host.appendChild(el);
62
+ s = {
63
+ el,
64
+ host,
65
+ activeTrigger: null,
66
+ currentPositions: [],
67
+ currentAnchorName: null
68
+ };
69
+ _singletons.set(host, s);
70
+ return s;
71
+ }
71
72
 
72
- // Ensure content is loaded before showing tooltip
73
- effect(() => {
74
- const store = Alpine.store('collections');
75
- if (store && typeof store.loadCollection === 'function' && !store[contentType]) {
76
- store.loadCollection(contentType);
77
- }
78
- });
79
- } else {
80
- // Check if expression contains HTML tags (indicating rich content)
81
- if (expression.includes('<') && expression.includes('>')) {
82
- // Treat as literal HTML string - escape any quotes to prevent syntax errors
83
- const escapedExpression = expression.replace(/'/g, "\\'");
84
- getTooltipContent = evaluateLater(`'${escapedExpression}'`);
85
- } else if (expression.includes('+') || expression.includes('`') || expression.includes('${')) {
86
- // Try to evaluate as a dynamic expression
87
- getTooltipContent = evaluateLater(expression);
73
+ // Restore a trigger's original anchor-name (captured before we overrode it).
74
+ // Scheduled with a long delay so the anchor stays valid through popover transitions.
75
+ const _pendingAnchorRestores = new WeakMap(); // trigger → timeoutId
76
+ const ANCHOR_RESTORE_DELAY_MS = 2000;
77
+
78
+ function scheduleAnchorRestore(trigger) {
79
+ const existing = _pendingAnchorRestores.get(trigger);
80
+ if (existing) clearTimeout(existing);
81
+ const id = setTimeout(() => {
82
+ _pendingAnchorRestores.delete(trigger);
83
+ if (trigger._tooltipOriginalAnchor) {
84
+ trigger.style.setProperty('anchor-name', trigger._tooltipOriginalAnchor);
88
85
  } else {
89
- // Use as static string
90
- getTooltipContent = evaluateLater(`'${expression}'`);
91
- }
92
- }
93
-
94
- effect(() => {
95
- // Generate a unique ID for the tooltip
96
- const tooltipCode = Math.random().toString(36).substr(2, 9);
97
- const tooltipId = `tooltip-${tooltipCode}`;
98
-
99
- // Store the original popovertarget if it exists, or check for x-dropdown
100
- let originalTarget = el.getAttribute('popovertarget');
101
-
102
- // If no popovertarget but has x-dropdown, that will become the target
103
- if (!originalTarget && el.hasAttribute('x-dropdown')) {
104
- originalTarget = el.getAttribute('x-dropdown');
86
+ trigger.style.removeProperty('anchor-name');
105
87
  }
88
+ }, ANCHOR_RESTORE_DELAY_MS);
89
+ _pendingAnchorRestores.set(trigger, id);
90
+ }
91
+ function cancelAnchorRestore(trigger) {
92
+ const id = _pendingAnchorRestores.get(trigger);
93
+ if (id) { clearTimeout(id); _pendingAnchorRestores.delete(trigger); }
94
+ }
106
95
 
107
- // Create the tooltip element (hint popovers coexist with auto menus/dialogs)
108
- const tooltip = document.createElement('div');
109
- tooltip.setAttribute('popover', 'hint');
110
- tooltip.setAttribute('id', tooltipId);
111
- tooltip.setAttribute('class', 'tooltip');
96
+ // ---- Controller ----
97
+ //
98
+ // Single pending-show timer shared across the whole plugin. If a trigger arms a
99
+ // show and the user moves to another trigger before it fires, the first timer is
100
+ // cancelled in favor of the new one. If the singleton is already visible, the new
101
+ // trigger updates it in place (chain mode) — no hide/show flicker.
102
+
103
+ let _showTimer = null;
104
+ let _pendingTrigger = null;
105
+ // Hide is deferred briefly so an incoming show on a different trigger can take
106
+ // over (chain-mode glide) instead of producing a hide/show flicker.
107
+ let _hideTimer = null;
108
+ const HIDE_DEFER_MS = 60;
109
+
110
+ function cancelPendingShow() {
111
+ if (_showTimer) { clearTimeout(_showTimer); _showTimer = null; }
112
+ _pendingTrigger = null;
113
+ }
114
+ function cancelPendingHide() {
115
+ if (_hideTimer) { clearTimeout(_hideTimer); _hideTimer = null; }
116
+ }
112
117
 
113
- // Store the original anchor name if it exists
114
- const originalAnchorName = el.style.getPropertyValue('anchor-name');
115
- const tooltipAnchor = `--tooltip-${tooltipCode}`;
116
118
 
117
- // Store original anchor name for restoration
118
- if (originalAnchorName) {
119
- el._originalAnchorName = originalAnchorName;
120
- }
119
+ // Update the singleton to point at a trigger (anchor, content, classes) and show it.
120
+ // Switches between triggers happen by re-anchoring — no positional animation. Any
121
+ // previous transform state is cleared so the tooltip sits squarely at its anchor.
122
+ function showSingletonFor(trigger, contentHtml, positions) {
123
+ const host = getTooltipHostForTrigger(trigger);
124
+ const s = getSingleton(host);
121
125
 
122
- // Handle positioning modifiers - preserve exact order and build class names like dropdown CSS
123
- const validPositions = ['top', 'bottom', 'start', 'end', 'center', 'corner'];
124
- const positions = modifiers.filter(mod => validPositions.includes(mod));
126
+ // Clear any residual transform state (defensive should already be clean)
127
+ s.el.style.transition = '';
128
+ s.el.style.translate = '';
125
129
 
126
- if (positions.length > 0) {
127
- // Build class name by joining modifiers with dashes (preserves original order)
128
- const positionClass = positions.join('-');
129
- tooltip.classList.add(positionClass);
130
- }
130
+ // Capture the trigger's original anchor-name so we can restore it later.
131
+ if (!trigger._tooltipOriginalAnchorCaptured) {
132
+ trigger._tooltipOriginalAnchor = trigger.style.getPropertyValue('anchor-name') || '';
133
+ trigger._tooltipOriginalAnchorCaptured = true;
134
+ }
135
+ cancelAnchorRestore(trigger);
131
136
 
132
- // Mount under the same popover as the trigger when applicable (see getTooltipHostForTrigger)
133
- getTooltipHostForTrigger(el).appendChild(tooltip);
134
-
135
- // State variables for managing tooltip behavior
136
- let showTimeout;
137
- let restoreAnchorTimeout = null;
138
- let anchorRestoreGeneration = 0;
139
- let isMouseDown = false;
140
- let isDynamic = expression.includes('+') || expression.includes('`') || expression.includes('${') || expression.startsWith('$x.');
141
- let isUpdatingContent = false;
142
-
143
- function restoreOriginalAnchor() {
144
- if (el._originalAnchorName) {
145
- el.style.setProperty('anchor-name', el._originalAnchorName);
146
- } else {
147
- el.style.removeProperty('anchor-name');
148
- }
149
- }
137
+ // Update position classes on the singleton. These drive the CSS positioning
138
+ // variants (top, bottom-end, etc.) defined in manifest.tooltip.css.
139
+ if (s.currentPositions.length) s.el.classList.remove(s.currentPositions.join('-'));
140
+ if (positions.length) s.el.classList.add(positions.join('-'));
141
+ s.currentPositions = positions;
150
142
 
151
- function cancelScheduledAnchorRestore() {
152
- anchorRestoreGeneration += 1;
153
- if (restoreAnchorTimeout !== null) {
154
- clearTimeout(restoreAnchorTimeout);
155
- restoreAnchorTimeout = null;
156
- }
157
- }
143
+ s.el.innerHTML = contentHtml || '';
158
144
 
159
- function scheduleAnchorRestoreAfterTooltipDismissal(restoreFn) {
160
- cancelScheduledAnchorRestore();
161
- const gen = anchorRestoreGeneration;
162
- const run = () => {
163
- if (gen !== anchorRestoreGeneration) return;
164
- restoreAnchorTimeout = null;
165
- restoreFn();
166
- };
167
- restoreAnchorTimeout = setTimeout(run, ANCHOR_RESTORE_DELAY_MS);
168
- }
145
+ // Anchor binding: give the trigger a unique anchor-name, point the singleton at it.
146
+ if (!trigger._tooltipAnchorName) {
147
+ const code = Math.random().toString(36).slice(2, 9);
148
+ trigger._tooltipAnchorName = `--tooltip-trigger-${code}`;
149
+ }
150
+ const anchorName = trigger._tooltipAnchorName;
151
+ trigger.style.setProperty('anchor-name', anchorName);
152
+ void trigger.offsetHeight; // reflow so anchor-name registers
153
+ s.el.style.setProperty('position-anchor', anchorName);
169
154
 
170
- function scheduleRestoreAnchorAfterClose() {
171
- scheduleAnchorRestoreAfterTooltipDismissal(() => restoreOriginalAnchor());
172
- }
155
+ s.activeTrigger = trigger;
156
+ s.currentAnchorName = anchorName;
173
157
 
174
- function ensureTooltipHostMatchesTrigger() {
175
- const host = getTooltipHostForTrigger(el);
176
- if (tooltip.parentNode !== host) {
177
- host.appendChild(tooltip);
178
- }
179
- }
158
+ if (!s.el.matches(':popover-open')) s.el.showPopover();
159
+ }
180
160
 
181
- // Function to update tooltip content - prevents double updates
182
- const updateTooltipContent = () => {
183
- // Prevent concurrent updates that cause flicker
184
- if (isUpdatingContent) return;
185
- isUpdatingContent = true;
186
-
187
- getTooltipContent(content => {
188
- tooltip.innerHTML = content || '';
189
- // Use requestAnimationFrame to ensure DOM update completes before allowing next update
190
- requestAnimationFrame(() => {
191
- isUpdatingContent = false;
192
- });
193
- });
194
- };
161
+ // Hide the singleton that's currently showing (if any), regardless of host.
162
+ function hideAnySingleton() {
163
+ document.querySelectorAll('.tooltip[popover="hint"]:popover-open').forEach(el => {
164
+ try { el.hidePopover(); } catch {}
165
+ });
166
+ markTooltipHidden();
167
+ }
195
168
 
196
- // For static content, set it once immediately to avoid delay
197
- // For dynamic content, set it only when showing to prevent double updates from reactivity
198
- if (!isDynamic) {
199
- updateTooltipContent();
200
- }
169
+ // ---- Directive ----
201
170
 
202
- el.addEventListener('mouseenter', () => {
203
- cancelScheduledAnchorRestore();
204
- clearTimeout(showTimeout);
205
- if (!isMouseDown) {
206
- const hoverDelay = getTooltipHoverDelay(el);
207
- showTimeout = setTimeout(() => {
208
- // Check if user is actively interacting with other popovers
209
- const hasOpenPopover = originalTarget && document.getElementById(originalTarget)?.matches(':popover-open');
210
-
211
- if (!isMouseDown && !tooltip.matches(':popover-open') && !hasOpenPopover) {
212
- // For dynamic content, update right before showing to ensure current value
213
- // The isUpdatingContent flag prevents the reactive callback from causing double updates
214
- if (isDynamic) {
215
- updateTooltipContent();
216
- }
217
-
218
- ensureTooltipHostMatchesTrigger();
219
-
220
- // Only manage anchor-name if element has other popover functionality
221
- if (originalTarget) {
222
- // Store current anchor name (dropdown may have set it by now)
223
- const currentAnchorName = el.style.getPropertyValue('anchor-name');
224
- if (currentAnchorName && currentAnchorName !== tooltipAnchor) {
225
- el._originalAnchorName = currentAnchorName;
226
- }
227
- }
228
-
229
- // Set anchor-name on element first
230
- el.style.setProperty('anchor-name', tooltipAnchor);
231
-
232
- // Force a reflow to ensure anchor is registered before setting position-anchor
233
- void el.offsetHeight;
234
-
235
- // Set position-anchor on tooltip
236
- tooltip.style.setProperty('position-anchor', tooltipAnchor);
237
-
238
- // Show tooltip without changing popovertarget
239
- tooltip.showPopover();
240
- }
241
- }, hoverDelay);
242
- }
243
- });
171
+ Alpine.directive('tooltip', (el, { modifiers, expression }, { effect, evaluateLater }) => {
244
172
 
245
- el.addEventListener('mouseleave', () => {
246
- clearTimeout(showTimeout);
247
- if (tooltip.matches(':popover-open')) {
248
- tooltip.hidePopover();
249
- scheduleRestoreAnchorAfterClose();
250
- }
251
- });
173
+ // --- Content evaluator ---
174
+ let getContent;
175
+ const isDynamic =
176
+ expression.startsWith('$x.') ||
177
+ (expression.includes('+') || expression.includes('`') || expression.includes('${'));
252
178
 
253
- el.addEventListener('mousedown', () => {
254
- isMouseDown = true;
255
- clearTimeout(showTimeout);
256
- if (tooltip.matches(':popover-open')) {
257
- tooltip.hidePopover();
179
+ if (expression.startsWith('$x.')) {
180
+ const path = expression.substring(3);
181
+ const [contentType] = path.split('.');
182
+ getContent = evaluateLater(expression);
183
+ effect(() => {
184
+ const store = Alpine.store('collections');
185
+ if (store && typeof store.loadCollection === 'function' && !store[contentType]) {
186
+ store.loadCollection(contentType);
258
187
  }
259
- scheduleRestoreAnchorAfterClose();
260
188
  });
189
+ } else if (expression.includes('<') && expression.includes('>')) {
190
+ // Literal HTML string
191
+ const escaped = expression.replace(/'/g, "\\'");
192
+ getContent = evaluateLater(`'${escaped}'`);
193
+ } else if (expression.includes('+') || expression.includes('`') || expression.includes('${')) {
194
+ getContent = evaluateLater(expression);
195
+ } else {
196
+ // Static literal — wrap in quotes so evaluateLater returns it verbatim
197
+ getContent = evaluateLater(`'${expression}'`);
198
+ }
261
199
 
262
- el.addEventListener('mouseup', () => {
263
- isMouseDown = false;
264
- });
200
+ // --- Positioning modifiers ---
201
+ const validPositions = ['top', 'bottom', 'start', 'end', 'center', 'corner'];
202
+ const positions = modifiers.filter(m => validPositions.includes(m));
265
203
 
266
- // Handle click events - hide tooltip but delay anchor restoration
267
- el.addEventListener('click', (e) => {
268
- clearTimeout(showTimeout);
204
+ // For non-dynamic content, cache once to avoid re-evaluating every show.
205
+ let cachedContent = null;
206
+ if (!isDynamic) {
207
+ getContent(v => { cachedContent = v; });
208
+ }
269
209
 
270
- // Hide tooltip if open
271
- if (tooltip.matches(':popover-open')) {
272
- tooltip.hidePopover();
210
+ // Resolves the content to show, calling the provided callback with the HTML string.
211
+ const resolveContent = (cb) => {
212
+ if (!isDynamic && cachedContent != null) { cb(cachedContent); return; }
213
+ getContent(v => cb(v));
214
+ };
215
+
216
+ // --- Event handlers ---
217
+ const requestShow = () => {
218
+ cancelPendingShow();
219
+ cancelPendingHide(); // incoming show cancels the deferred hide — this is the glide takeover
220
+ _pendingTrigger = el;
221
+ // Chain mode: if the singleton is still open (hide was deferred, about to
222
+ // happen), or was just dismissed within the grace window, show now.
223
+ const anyOpen = document.querySelector('.tooltip[popover="hint"]:popover-open');
224
+ const delay = (anyOpen || isInChainWindow()) ? 0 : getTooltipHoverDelay(el);
225
+ _showTimer = setTimeout(() => {
226
+ _showTimer = null;
227
+ if (_pendingTrigger !== el) return;
228
+ const triggerTargetId = el.getAttribute('popovertarget') || el.getAttribute('x-dropdown');
229
+ if (triggerTargetId) {
230
+ const t = document.getElementById(triggerTargetId);
231
+ if (t && t.matches && t.matches(':popover-open')) return;
273
232
  }
274
-
275
- // After computed transition time: restore anchor only if no popover opened from this click
276
- scheduleAnchorRestoreAfterTooltipDismissal(() => {
277
- if (originalTarget) {
278
- const targetPopover = document.getElementById(originalTarget);
279
- const isPopoverOpen = targetPopover?.matches(':popover-open');
280
- if (!targetPopover || !isPopoverOpen) {
281
- restoreOriginalAnchor();
282
- }
283
- } else {
284
- restoreOriginalAnchor();
285
- }
233
+ resolveContent(html => {
234
+ showSingletonFor(el, html, positions);
286
235
  });
287
- });
288
-
289
- // Listen for other popovers opening and close tooltip if needed
290
- const handlePopoverOpen = (event) => {
291
- // If another popover opens and it's not our tooltip, close our tooltip
292
- if (event.target !== tooltip && tooltip.matches(':popover-open')) {
293
- tooltip.hidePopover();
294
- scheduleRestoreAnchorAfterClose();
295
- }
296
- };
297
-
298
- // Add global listener for popover events (only if not already added)
299
- if (!el._tooltipPopoverListener) {
300
- document.addEventListener('toggle', handlePopoverOpen);
301
- el._tooltipPopoverListener = handlePopoverOpen;
302
- }
303
-
304
- // Cleanup function for when element is removed
305
- const cleanup = () => {
306
- cancelScheduledAnchorRestore();
307
- if (el._tooltipPopoverListener) {
308
- document.removeEventListener('toggle', el._tooltipPopoverListener);
309
- delete el._tooltipPopoverListener;
310
- }
311
- if (tooltip && tooltip.parentElement) {
312
- tooltip.remove();
236
+ }, delay);
237
+ };
238
+
239
+ const requestHide = () => {
240
+ cancelPendingShow();
241
+ // Defer the actual hide briefly so an incoming show on a different trigger
242
+ // can take over (chain mode: immediate show) rather than flicker-close.
243
+ cancelPendingHide();
244
+ _hideTimer = setTimeout(() => {
245
+ _hideTimer = null;
246
+ const host = getTooltipHostForTrigger(el);
247
+ const s = _singletons.get(host);
248
+ if (s && s.activeTrigger === el && s.el.matches(':popover-open')) {
249
+ s.el.hidePopover();
250
+ s.activeTrigger = null;
251
+ markTooltipHidden();
252
+ scheduleAnchorRestore(el);
313
253
  }
314
- };
315
-
316
- // Store cleanup function for manual cleanup if needed
317
- el._tooltipCleanup = cleanup;
318
- });
254
+ }, HIDE_DEFER_MS);
255
+ };
256
+
257
+ // Mouse interactions
258
+ el.addEventListener('mouseenter', requestShow);
259
+ el.addEventListener('mouseleave', requestHide);
260
+
261
+ // Mousedown/click: always hide immediately; scheduleAnchorRestore so the
262
+ // trigger's anchor-name stays valid long enough for any dropdown popover
263
+ // it launches to position itself correctly.
264
+ const hideAndScheduleRestore = () => {
265
+ cancelPendingShow();
266
+ hideAnySingleton();
267
+ scheduleAnchorRestore(el);
268
+ };
269
+ el.addEventListener('mousedown', hideAndScheduleRestore);
270
+ el.addEventListener('click', hideAndScheduleRestore);
319
271
  });
272
+
273
+ // Global: when ANY other popover opens, close the singleton(s). Dropdowns and
274
+ // dialogs take precedence over tooltips.
275
+ document.addEventListener('toggle', (event) => {
276
+ if (event.newState !== 'open') return;
277
+ const t = event.target;
278
+ if (t.classList && t.classList.contains('tooltip') && t.getAttribute('popover') === 'hint') return;
279
+ hideAnySingleton();
280
+ }, true);
320
281
  }
321
282
 
322
- // Track initialization to prevent duplicates
283
+ // ---- Plugin init boilerplate ----
284
+
323
285
  let tooltipPluginInitialized = false;
324
286
 
325
287
  function ensureTooltipPluginInitialized() {
326
288
  if (tooltipPluginInitialized) return;
327
289
  if (!window.Alpine || typeof window.Alpine.directive !== 'function') return;
328
-
329
290
  tooltipPluginInitialized = true;
330
291
  initializeTooltipPlugin();
331
- // Do not call Alpine.initTree() on [x-tooltip] elements here. That initializes
332
- // them in isolation and breaks scope (e.g. "tab is not defined"). Alpine will
333
- // process the full tree from the root, so [x-tooltip] elements get the correct
334
- // scope. Dynamically loaded components are already initialized by the component
335
- // processor with initTree on the swapped-in root.
336
292
  }
337
293
 
338
- // Expose on window for loader to call if needed
339
294
  window.ensureTooltipPluginInitialized = ensureTooltipPluginInitialized;
340
295
 
341
- // Handle both DOMContentLoaded and alpine:init
342
296
  if (document.readyState === 'loading') {
343
297
  document.addEventListener('DOMContentLoaded', ensureTooltipPluginInitialized);
344
298
  }
345
-
346
299
  document.addEventListener('alpine:init', ensureTooltipPluginInitialized);
347
300
 
348
- // If Alpine is already initialized when this script loads, initialize immediately
349
301
  if (window.Alpine && typeof window.Alpine.directive === 'function') {
350
302
  setTimeout(ensureTooltipPluginInitialized, 0);
351
303
  } else if (document.readyState === 'complete') {
352
- // If document is already loaded but Alpine isn't ready yet, wait for it
353
304
  const checkAlpine = setInterval(() => {
354
305
  if (window.Alpine && typeof window.Alpine.directive === 'function') {
355
306
  clearInterval(checkAlpine);
356
307
  ensureTooltipPluginInitialized();
357
308
  }
358
- }, 10);
309
+ }, 50);
359
310
  setTimeout(() => clearInterval(checkAlpine), 5000);
360
- }
311
+ }
@@ -85,7 +85,7 @@
85
85
 
86
86
  /* Selected */
87
87
  :where(.selected) {
88
- background-color: color-mix(in oklch, var(--color-field-surface, oklch(91.79% 0.0029 264.26)) 25%, transparent);
88
+ background-color: var(--color-field-surface, oklch(91.79% 0.0029 264.26))
89
89
  }
90
90
 
91
91
  /* Transparent */
@@ -173,6 +173,20 @@
173
173
  }
174
174
  }
175
175
 
176
+ /* No spinners */
177
+ .no-spinner {
178
+ -moz-appearance: textfield !important;
179
+ appearance: textfield !important;
180
+
181
+ &::-webkit-inner-spin-button,
182
+ &::-webkit-outer-spin-button {
183
+ -webkit-appearance: none;
184
+ appearance: none;
185
+ display: none;
186
+ margin: 0;
187
+ }
188
+ }
189
+
176
190
  /* Banner overlays */
177
191
  :where(.overlay-dark, .overlay-light) {
178
192
  position: relative;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.57",
3
+ "version": "0.5.59",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",