// Nova toolbar interactions
// - dropdown menus via [data-dropdown]
// - mobile menu toggle via [data-mobile-toggle] + #mobileMenu
// Alpine.js — powers x-data/x-show/@click in Blade layouts (e.g. cookie banner, toasts).
// Guard: don't start a second instance if app.js already loaded Alpine on this page.
import Alpine from 'alpinejs';
import React from 'react';
import { createRoot } from 'react-dom/client';
if (!window.Alpine) {
window.Alpine = Alpine;
Alpine.start();
}
// Gallery navigation context: stores artwork list for prev/next on artwork page
import './lib/nav-context.js';
import { sendTagInteractionEvent } from './lib/tagAnalytics';
function mountStoryEditor() {
var storyEditorRoot = document.getElementById('story-editor-react-root');
if (!storyEditorRoot) return;
if (storyEditorRoot.dataset.reactMounted === 'true') return;
var mode = storyEditorRoot.getAttribute('data-mode') || 'create';
var storyRaw = storyEditorRoot.getAttribute('data-story') || '{}';
var storyTypesRaw = storyEditorRoot.getAttribute('data-story-types') || '[]';
var endpointsRaw = storyEditorRoot.getAttribute('data-endpoints') || '{}';
var csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
var initialStory = {};
var storyTypes = [];
var endpoints = {};
try {
initialStory = JSON.parse(storyRaw);
storyTypes = JSON.parse(storyTypesRaw);
endpoints = JSON.parse(endpointsRaw);
} catch (_error) {
// If parsing fails, the editor falls back to component defaults.
}
storyEditorRoot.dataset.reactMounted = 'true';
void import('./components/editor/StoryEditor')
.then(function (module) {
var StoryEditor = module.default;
createRoot(storyEditorRoot).render(
React.createElement(StoryEditor, {
mode: mode,
initialStory: initialStory,
storyTypes: storyTypes,
endpoints: endpoints,
csrfToken: csrfToken,
})
);
})
.catch(function () {
storyEditorRoot.dataset.reactMounted = 'false';
storyEditorRoot.innerHTML = '
Failed to load editor. Please refresh the page.
';
});
}
mountStoryEditor();
function initTagsSearchAssist() {
var roots = document.querySelectorAll('[data-tags-search-root]');
if (!roots.length) return;
var recentSearchesKey = 'skinbase.tags.recent-searches';
function findClosest(el, selector) {
while (el && el.nodeType === 1) {
if (el.matches(selector)) return el;
el = el.parentElement;
}
return null;
}
function escapeHtml(value) {
return String(value || '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
roots.forEach(function (root, rootIndex) {
var form = findClosest(root, '[data-tags-search-form]');
var input = root.querySelector('[data-tags-search-input]');
var panel = root.querySelector('[data-tags-search-panel]');
var title = root.querySelector('[data-tags-search-title]');
var results = root.querySelector('[data-tags-search-results]');
var endpoint = root.getAttribute('data-search-endpoint') || '/api/tags/search';
var popularEndpoint = root.getAttribute('data-popular-endpoint') || '/api/tags/popular';
var optionIdPrefix = 'tags-search-option-' + rootIndex + '-';
var debounceTimer = null;
var abortController = null;
var latestQuery = '';
var activeIndex = -1;
if (!input || !panel || !results) return;
function readRecentSearches() {
try {
var parsed = JSON.parse(window.localStorage.getItem(recentSearchesKey) || '[]');
return Array.isArray(parsed) ? parsed.filter(Boolean).slice(0, 5) : [];
} catch (_error) {
return [];
}
}
function writeRecentSearch(query) {
var normalized = (query || '').trim();
if (!normalized) return;
var next = readRecentSearches().filter(function (item) {
return item.toLowerCase() !== normalized.toLowerCase();
});
next.unshift(normalized);
try {
window.localStorage.setItem(recentSearchesKey, JSON.stringify(next.slice(0, 5)));
} catch (_error) {
// Ignore storage failures silently.
}
}
function clearRecentSearches() {
try {
window.localStorage.removeItem(recentSearchesKey);
} catch (_error) {
// Ignore storage failures silently.
}
}
function removeRecentSearch(query) {
var normalized = (query || '').trim().toLowerCase();
if (!normalized) return;
var next = readRecentSearches().filter(function (item) {
return item.toLowerCase() !== normalized;
});
try {
window.localStorage.setItem(recentSearchesKey, JSON.stringify(next));
} catch (_error) {
// Ignore storage failures silently.
}
}
function getItems() {
return Array.prototype.slice.call(results.querySelectorAll('[data-tags-search-item]'));
}
function setActiveItem(nextIndex) {
var items = getItems();
activeIndex = nextIndex;
items.forEach(function (item, index) {
var active = index === nextIndex;
item.classList.toggle('bg-white/[0.06]', active);
item.classList.toggle('text-white', active);
item.setAttribute('aria-selected', active ? 'true' : 'false');
if (!item.id) {
item.id = optionIdPrefix + index;
}
if (active) {
item.scrollIntoView({ block: 'nearest' });
}
});
if (nextIndex >= 0 && items[nextIndex]) {
input.setAttribute('aria-activedescendant', items[nextIndex].id);
} else {
input.removeAttribute('aria-activedescendant');
}
}
function focusItem(nextIndex) {
var items = getItems();
if (!items.length) return;
var boundedIndex = Math.max(0, Math.min(nextIndex, items.length - 1));
setActiveItem(boundedIndex);
items[boundedIndex].focus();
}
function setExpanded(expanded) {
input.setAttribute('aria-expanded', expanded ? 'true' : 'false');
panel.classList.toggle('hidden', !expanded);
if (!expanded) {
activeIndex = -1;
setActiveItem(-1);
}
}
function clearResults() {
results.innerHTML = '';
activeIndex = -1;
}
function hidePanel() {
setExpanded(false);
}
function renderLoadingState(query) {
title.textContent = query ? 'Searching tags' : 'Loading suggestions';
results.setAttribute('aria-busy', 'true');
results.innerHTML = '';
setExpanded(true);
setActiveItem(-1);
}
function fetchJson(url, signal) {
return fetch(url, {
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' },
signal: signal,
}).then(function (response) {
if (!response.ok) throw new Error('Failed to load tag suggestions');
return response.json();
});
}
function renderRecentSearches(items) {
if (!items.length) return '';
return ''
+ '
'
+ '
Recent searches
'
+ '
'
+ '
'
+ '
'
+ items.map(function (item) {
var encoded = encodeURIComponent(item);
var escaped = escapeHtml(item);
return '
'
+ '' + escaped + ''
+ ''
+ '';
}).join('')
+ '
'
+ '
';
}
function renderRescueSuggestions(items, query) {
if (!items.length) return '';
return ''
+ '
Try these instead
'
+ '
'
+ '
'
+ '
';
}
function renderNoMatchState(query, rescueItems) {
title.textContent = 'No matching tags';
results.setAttribute('aria-busy', 'false');
results.innerHTML = 'No direct matches for ' + escapeHtml(query) + '. Try a broader keyword or jump into one of these popular tags.
'
+ renderRescueSuggestions(rescueItems || [], query);
setExpanded(true);
setActiveItem(-1);
}
function renderItems(items, query) {
clearResults();
results.setAttribute('aria-busy', 'false');
var recentSearches = !query ? readRecentSearches() : [];
if (!items.length && recentSearches.length) {
title.textContent = 'Recent searches';
results.innerHTML = renderRecentSearches(recentSearches);
setExpanded(true);
setActiveItem(-1);
return;
}
if (!items.length) {
return;
}
title.textContent = query ? 'Matching tags' : (recentSearches.length ? 'Recent and popular' : 'Popular tags');
if (recentSearches.length) {
results.insertAdjacentHTML('beforeend', renderRecentSearches(recentSearches));
}
items.forEach(function (item, index) {
var link = document.createElement('a');
var itemName = escapeHtml(item.name || '');
var itemSlug = escapeHtml(item.slug || '');
var recentClicks = Number(item.recent_clicks || 0);
var metricLabel = recentClicks > 0
? recentClicks.toLocaleString() + ' recent'
: Number(item.usage_count || 0).toLocaleString() + ' uses';
link.href = '/tag/' + item.slug;
link.className = 'flex items-center justify-between gap-3 rounded-xl px-3 py-3 text-sm text-white/72 transition hover:bg-white/[0.06] hover:text-white';
link.setAttribute('data-tags-search-item', '');
link.setAttribute('data-tags-search-surface', 'search_suggestion');
link.setAttribute('data-tags-search-tag', item.slug || '');
link.setAttribute('data-tags-search-query', item.name || item.slug || '');
link.setAttribute('data-tags-search-input', query || '');
link.setAttribute('data-tags-search-position', String(index + 1));
link.setAttribute('aria-selected', 'false');
link.setAttribute('role', 'option');
link.tabIndex = -1;
link.innerHTML =
''
+ '#'
+ '' + itemName + '#' + itemSlug + ''
+ ''
+ '' + metricLabel + '';
results.appendChild(link);
});
setExpanded(true);
setActiveItem(-1);
}
function fetchSuggestions(query) {
if (abortController) abortController.abort();
abortController = new AbortController();
latestQuery = query;
renderLoadingState(query);
var params = new URLSearchParams();
if (query) params.set('q', query);
fetchJson(endpoint + (params.toString() ? ('?' + params.toString()) : ''), abortController.signal)
.then(function (payload) {
if (input.value.trim() !== latestQuery) return;
var items = Array.isArray(payload.data) ? payload.data.slice(0, 8) : [];
if (items.length || query === '') {
renderItems(items, query);
return;
}
var popularParams = new URLSearchParams();
popularParams.set('limit', '4');
return fetchJson(popularEndpoint + '?' + popularParams.toString(), abortController.signal)
.then(function (popularPayload) {
if (input.value.trim() !== latestQuery) return;
renderNoMatchState(query, Array.isArray(popularPayload.data) ? popularPayload.data : []);
});
})
.catch(function (error) {
if (error && error.name === 'AbortError') return;
hidePanel();
});
}
input.addEventListener('focus', function () {
fetchSuggestions(input.value.trim());
});
input.addEventListener('input', function () {
var query = input.value.trim();
window.clearTimeout(debounceTimer);
debounceTimer = window.setTimeout(function () {
fetchSuggestions(query);
}, 180);
});
input.addEventListener('keydown', function (event) {
var items = getItems();
if ((event.key === 'ArrowDown' || event.key === 'ArrowUp') && items.length) {
event.preventDefault();
if (event.key === 'ArrowDown') {
setActiveItem(activeIndex < 0 ? 0 : Math.min(activeIndex + 1, items.length - 1));
} else {
setActiveItem(activeIndex < 0 ? items.length - 1 : Math.max(activeIndex - 1, 0));
}
return;
}
if (event.key === 'Enter' && activeIndex >= 0 && items[activeIndex]) {
event.preventDefault();
writeRecentSearch(items[activeIndex].getAttribute('data-tags-search-query') || input.value);
window.location.href = items[activeIndex].href;
return;
}
if (event.key === 'Escape') {
hidePanel();
}
});
results.addEventListener('keydown', function (event) {
var items = getItems();
var currentIndex = items.indexOf(document.activeElement);
if (event.key === 'ArrowDown' && items.length) {
event.preventDefault();
focusItem(currentIndex + 1);
return;
}
if (event.key === 'ArrowUp' && items.length) {
event.preventDefault();
if (currentIndex <= 0) {
setActiveItem(-1);
input.focus();
} else {
focusItem(currentIndex - 1);
}
return;
}
if (event.key === 'Escape') {
event.preventDefault();
hidePanel();
input.focus();
}
});
results.addEventListener('mouseover', function (event) {
var item = findClosest(event.target, '[data-tags-search-item]');
if (!item) return;
var items = getItems();
setActiveItem(items.indexOf(item));
});
results.addEventListener('click', function (event) {
var clearButton = findClosest(event.target, '[data-tags-search-clear-recent]');
if (clearButton) {
event.preventDefault();
clearRecentSearches();
fetchSuggestions(input.value.trim());
return;
}
var removeButton = findClosest(event.target, '[data-tags-search-remove-recent]');
if (removeButton) {
event.preventDefault();
removeRecentSearch(removeButton.getAttribute('data-tags-search-query') || '');
fetchSuggestions(input.value.trim());
return;
}
var item = findClosest(event.target, '[data-tags-search-item]');
if (!item) return;
var query = item.getAttribute('data-tags-search-query') || item.textContent || '';
writeRecentSearch(query);
var surface = item.getAttribute('data-tags-search-surface') || '';
var tagSlug = item.getAttribute('data-tags-search-tag') || '';
if (surface && (tagSlug || surface === 'recent_search')) {
sendTagInteractionEvent({
event_type: 'click',
surface: surface,
tag_slug: tagSlug || null,
query: item.getAttribute('data-tags-search-input') || item.getAttribute('data-tags-search-query') || input.value.trim() || null,
position: Number(item.getAttribute('data-tags-search-position') || 0) || null,
occurred_at: new Date().toISOString(),
});
}
});
if (form) {
form.addEventListener('submit', function () {
writeRecentSearch(input.value);
});
}
document.addEventListener('click', function (event) {
if (!root.contains(event.target)) {
hidePanel();
}
});
});
}
initTagsSearchAssist();
function initTagAnalyticsLinks() {
document.addEventListener('click', function (event) {
var el = event.target;
while (el && el.nodeType === 1) {
if (el.matches('[data-tag-analytics-link]')) {
var tagSlug = el.getAttribute('data-tag-analytics-tag') || '';
var surface = el.getAttribute('data-tag-analytics-surface') || '';
if (tagSlug && surface) {
sendTagInteractionEvent({
event_type: 'click',
surface: surface,
tag_slug: tagSlug,
source_tag_slug: el.getAttribute('data-tag-analytics-source-tag') || null,
position: Number(el.getAttribute('data-tag-analytics-position') || 0) || null,
occurred_at: new Date().toISOString(),
});
}
return;
}
el = el.parentElement;
}
});
}
initTagAnalyticsLinks();
(function () {
function initBlurPreviewImages() {
var selector = 'img[data-blur-preview]';
function markLoaded(img) {
if (!img) return;
img.classList.remove('blur-sm', 'scale-[1.02]');
img.classList.add('is-loaded');
}
document.querySelectorAll(selector).forEach(function (img) {
if (img.complete && img.naturalWidth > 0) {
markLoaded(img);
return;
}
img.addEventListener('load', function () { markLoaded(img); }, { once: true });
img.addEventListener('error', function () { markLoaded(img); }, { once: true });
});
document.addEventListener('load', function (event) {
var target = event.target;
if (target && target.matches && target.matches(selector)) {
markLoaded(target);
}
}, true);
}
initBlurPreviewImages();
function closest(el, selector) {
while (el && el.nodeType === 1) {
if (el.matches(selector)) return el;
el = el.parentElement;
}
return null;
}
function canHover() {
return window.matchMedia && window.matchMedia('(hover: hover) and (pointer: fine)').matches;
}
function setExpanded(toggleEl, expanded) {
if (!toggleEl) return;
toggleEl.setAttribute('aria-expanded', expanded ? 'true' : 'false');
}
function closeAllDropdowns(except) {
var dropdowns = document.querySelectorAll('[data-dropdown]');
dropdowns.forEach(function (dropdown) {
if (except && dropdown === except) return;
var menu = dropdown.querySelector('[data-dropdown-menu]');
var toggle = dropdown.querySelector('[data-dropdown-toggle]');
if (menu) menu.classList.remove('is-open');
setExpanded(toggle, false);
// Close any submenus
dropdown.querySelectorAll('[data-submenu-menu]').forEach(function (sm) {
sm.classList.add('hidden');
});
dropdown.querySelectorAll('[data-submenu-toggle]').forEach(function (st) {
setExpanded(st, false);
});
});
}
function openDropdown(dropdown) {
var menu = dropdown.querySelector('[data-dropdown-menu]');
var toggle = dropdown.querySelector('[data-dropdown-toggle]');
if (!menu || !toggle) return;
closeAllDropdowns(dropdown);
menu.classList.add('is-open');
setExpanded(toggle, true);
}
function closeDropdown(dropdown) {
var menu = dropdown.querySelector('[data-dropdown-menu]');
var toggle = dropdown.querySelector('[data-dropdown-toggle]');
if (menu) menu.classList.remove('is-open');
setExpanded(toggle, false);
}
function toggleDropdown(dropdown) {
var menu = dropdown.querySelector('[data-dropdown-menu]');
var toggle = dropdown.querySelector('[data-dropdown-toggle]');
if (!menu || !toggle) return;
var isOpen = menu.classList.contains('is-open');
closeAllDropdowns(isOpen ? null : dropdown);
if (isOpen) {
menu.classList.remove('is-open');
setExpanded(toggle, false);
} else {
menu.classList.add('is-open');
setExpanded(toggle, true);
}
}
function getMobileMenu() {
return document.getElementById('mobileMenu');
}
function setMobileToggleVisual(isOpen) {
var toggle = document.querySelector('[data-mobile-toggle]') || document.getElementById('btnSidebar');
if (!toggle) return;
setExpanded(toggle, !!isOpen);
var hamburgerIcon = toggle.querySelector('[data-mobile-icon-hamburger]');
var closeIcon = toggle.querySelector('[data-mobile-icon-close]');
if (hamburgerIcon) hamburgerIcon.classList.toggle('hidden', !!isOpen);
if (closeIcon) closeIcon.classList.toggle('hidden', !isOpen);
}
function closeMobileMenu() {
var menu = getMobileMenu();
if (!menu) return;
menu.classList.add('hidden');
setMobileToggleVisual(false);
}
function toggleMobileMenu() {
var menu = getMobileMenu();
if (!menu) return;
var isOpen = !menu.classList.contains('hidden');
if (isOpen) {
closeMobileMenu();
} else {
menu.classList.remove('hidden');
setMobileToggleVisual(true);
closeAllDropdowns();
}
}
document.addEventListener('click', function (e) {
var dropdownToggle = closest(e.target, '[data-dropdown-toggle]');
// legacy shorthand toggles: data-dd="name" -> menu id = dd-name
var legacyToggle = closest(e.target, '[data-dd]');
if (dropdownToggle) {
// On pointer/hover-capable devices prefer hover; ignore mouse clicks
if (canHover() && e.detail > 0) {
// allow keyboard activation (e.detail === 0) to fall through
return;
}
e.preventDefault();
var dropdown = closest(dropdownToggle, '[data-dropdown]');
if (dropdown) toggleDropdown(dropdown);
return;
}
if (legacyToggle) {
// On pointer/hover-capable devices prefer hover; ignore mouse clicks
if (canHover() && e.detail > 0) {
return;
}
e.preventDefault();
var ddName = legacyToggle.getAttribute('data-dd');
if (!ddName) return;
var menu = document.getElementById('dd-' + ddName);
if (!menu) return;
// treat this pair (toggle + menu) similarly to our dropdown API
var isOpen = menu.classList.contains('is-open');
// close other dropdowns
closeAllDropdowns();
// also close other legacy (data-dd) menus
document.querySelectorAll('[data-dd]').forEach(function (other) {
if (other === legacyToggle) return;
var otherId = other.getAttribute('data-dd');
var otherMenu = otherId ? document.getElementById('dd-' + otherId) : null;
if (otherMenu) otherMenu.classList.remove('is-open');
setExpanded(other, false);
});
if (isOpen) {
menu.classList.remove('is-open');
setExpanded(legacyToggle, false);
} else {
menu.classList.add('is-open');
setExpanded(legacyToggle, true);
}
return;
}
var mobileToggle = closest(e.target, '[data-mobile-toggle]');
if (mobileToggle) {
e.preventDefault();
toggleMobileMenu();
return;
}
var mobileSectionToggle = closest(e.target, '[data-mobile-section-toggle]');
if (mobileSectionToggle) {
e.preventDefault();
var panelId = mobileSectionToggle.getAttribute('aria-controls');
var panel = panelId ? document.getElementById(panelId) : null;
if (!panel) return;
var wasOpen = !panel.classList.contains('hidden');
var menuRoot = getMobileMenu();
// Keep mobile navigation tidy: close all sections first.
if (menuRoot) {
menuRoot.querySelectorAll('[data-mobile-section-panel]').forEach(function (el) {
el.classList.add('hidden');
});
menuRoot.querySelectorAll('[data-mobile-section-toggle]').forEach(function (btn) {
setExpanded(btn, false);
var icon = btn.querySelector('[data-mobile-section-icon]');
if (icon) icon.classList.remove('rotate-180');
});
}
// If it was closed, open it. If it was open, it stays closed (toggle behavior).
if (!wasOpen) {
panel.classList.remove('hidden');
setExpanded(mobileSectionToggle, true);
var currentIcon = mobileSectionToggle.querySelector('[data-mobile-section-icon]');
if (currentIcon) currentIcon.classList.add('rotate-180');
}
return;
}
// Submenu toggle (touch/click fallback)
var submenuToggle = closest(e.target, '[data-submenu-toggle]');
if (submenuToggle) {
if (canHover()) {
// On desktop, submenu opens on hover via CSS.
e.preventDefault();
return;
}
e.preventDefault();
var submenu = closest(submenuToggle, '[data-submenu]');
if (!submenu) return;
var menu = submenu.querySelector('[data-submenu-menu]');
if (!menu) return;
// Close other submenus within the same dropdown
var dropdown = closest(submenuToggle, '[data-dropdown]');
if (dropdown) {
dropdown.querySelectorAll('[data-submenu-menu]').forEach(function (sm) {
if (sm !== menu) sm.classList.add('hidden');
});
dropdown.querySelectorAll('[data-submenu-toggle]').forEach(function (st) {
if (st !== submenuToggle) setExpanded(st, false);
});
}
var isOpen = !menu.classList.contains('hidden');
if (isOpen) {
menu.classList.add('hidden');
setExpanded(submenuToggle, false);
} else {
menu.classList.remove('hidden');
setExpanded(submenuToggle, true);
}
return;
}
if (!closest(e.target, '[data-dropdown]')) {
closeAllDropdowns();
}
// Close mobile menu when tapping outside of it and outside the hamburger toggle.
var mobileMenu = getMobileMenu();
var mobileToggle = closest(e.target, '[data-mobile-toggle]') || closest(e.target, '#btnSidebar');
if (mobileMenu && !mobileMenu.classList.contains('hidden') && !mobileToggle && !closest(e.target, '#mobileMenu')) {
closeMobileMenu();
}
});
// Hover-to-open for desktop pointers
var hoverCloseTimers = new WeakMap();
function clearHoverTimer(dropdown) {
var t = hoverCloseTimers.get(dropdown);
if (t) window.clearTimeout(t);
hoverCloseTimers.delete(dropdown);
}
function scheduleClose(dropdown) {
clearHoverTimer(dropdown);
hoverCloseTimers.set(
dropdown,
window.setTimeout(function () {
closeMenuElement(dropdown);
}, 140)
);
}
// Close a menu element or its parent dropdown wrapper.
function closeMenuElement(el) {
if (!el) return;
// If this is a dropdown wrapper, find the menu inside
if (el.hasAttribute && el.hasAttribute('data-dropdown')) {
var menu = el.querySelector('[data-dropdown-menu]');
var toggle = el.querySelector('[data-dropdown-toggle]');
if (menu) menu.classList.remove('is-open');
setExpanded(toggle, false);
// also close submenus inside
el.querySelectorAll('[data-submenu-menu]').forEach(function (sm) {
sm.classList.add('hidden');
});
el.querySelectorAll('[data-submenu-toggle]').forEach(function (st) {
setExpanded(st, false);
});
return;
}
// If it's a menu element (e.g., legacy id=dd-name) hide it and try to find its toggle
var menuEl = el;
if (!menuEl.id && el.getAttribute && el.getAttribute('data-dropdown-menu')) {
// explicit menu element
}
// hide the element if possible
try { menuEl.classList.remove('is-open'); } catch (e) {}
// Try to map back to a toggle: id like dd-name -> data-dd="name"
if (menuEl.id && menuEl.id.indexOf('dd-') === 0) {
var name = menuEl.id.slice(3);
var toggle = document.querySelector('[data-dd="' + name + '"]');
if (toggle) setExpanded(toggle, false);
} else {
// fallback: if menu is inside a [data-dropdown], handled above; nothing more to do
}
}
function bindHoverHandlers() {
if (!canHover()) return;
document.querySelectorAll('[data-dropdown]').forEach(function (dropdown) {
dropdown.addEventListener('mouseenter', function () {
clearHoverTimer(dropdown);
openDropdown(dropdown);
});
dropdown.addEventListener('mouseleave', function () {
scheduleClose(dropdown);
});
});
// legacy hover binding for shorthand toggles (data-dd)
document.querySelectorAll('[data-dd]').forEach(function (el) {
var ddName = el.getAttribute('data-dd');
if (!ddName) return;
var menu = document.getElementById('dd-' + ddName);
if (!menu) return;
// when pointer enters either toggle or menu, open
function enter() {
clearHoverTimer(menu);
// Instantly close any other open legacy dropdown to prevent overlap
document.querySelectorAll('[data-dd]').forEach(function (other) {
if (other === el) return;
var otherId = other.getAttribute('data-dd');
var otherMenu = otherId ? document.getElementById('dd-' + otherId) : null;
if (otherMenu) otherMenu.classList.remove('is-open');
setExpanded(other, false);
});
menu.classList.add('is-open');
setExpanded(el, true);
}
function leave() {
scheduleClose(menu);
}
el.addEventListener('mouseenter', enter);
el.addEventListener('mouseleave', leave);
menu.addEventListener('mouseenter', enter);
menu.addEventListener('mouseleave', leave);
});
}
bindHoverHandlers();
// Submenu hover handlers: ensure flyouts open on pointer devices
if (canHover()) {
document.querySelectorAll('[data-submenu]').forEach(function (group) {
var toggle = group.querySelector('[data-submenu-toggle]');
var menu = group.querySelector('[data-submenu-menu]');
if (!menu) return;
group.addEventListener('mouseenter', function () {
menu.classList.remove('hidden');
if (toggle) setExpanded(toggle, true);
});
group.addEventListener('mouseleave', function () {
menu.classList.add('hidden');
if (toggle) setExpanded(toggle, false);
});
});
}
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
closeAllDropdowns();
closeMobileMenu();
});
window.addEventListener('resize', function () {
if (window.matchMedia('(min-width: 768px)').matches) {
closeMobileMenu();
}
});
})();