Add standardized HTMX conventions, interaction patterns, and migration guide for ThrillWiki UX

This commit is contained in:
pacnpal
2025-12-22 16:56:27 -05:00
parent 2e35f8c5d9
commit ae31e889d7
144 changed files with 25792 additions and 4440 deletions

View File

@@ -7,6 +7,7 @@
* - Mobile menu functionality
* - Flash message handling
* - Tooltip initialization
* - Global HTMX loading state management
*/
// =============================================================================
@@ -171,15 +172,139 @@ document.addEventListener('DOMContentLoaded', () => {
tooltipEl.className = 'absolute z-50 px-2 py-1 text-sm text-white bg-gray-900 rounded tooltip';
tooltipEl.textContent = text;
document.body.appendChild(tooltipEl);
const rect = e.target.getBoundingClientRect();
tooltipEl.style.top = rect.bottom + 5 + 'px';
tooltipEl.style.left = rect.left + (rect.width - tooltipEl.offsetWidth) / 2 + 'px';
});
tooltip.addEventListener('mouseleave', () => {
const tooltips = document.querySelectorAll('.tooltip');
tooltips.forEach(t => t.remove());
});
});
});
// =============================================================================
// HTMX Loading State Management
// =============================================================================
/**
* Global HTMX Loading State Management
*
* Provides consistent loading state handling across the application:
* - Adds 'htmx-loading' class to body during requests
* - Manages button disabled states during form submissions
* - Handles search input loading states with debouncing
* - Provides skeleton screen swap utilities
*/
document.addEventListener('DOMContentLoaded', () => {
// Track active HTMX requests
let activeRequests = 0;
/**
* Add global loading class to body during HTMX requests
*/
document.body.addEventListener('htmx:beforeRequest', (evt) => {
activeRequests++;
document.body.classList.add('htmx-loading');
// Disable submit buttons within the target element
const target = evt.target;
if (target.tagName === 'FORM' || target.closest('form')) {
const form = target.tagName === 'FORM' ? target : target.closest('form');
const submitBtn = form.querySelector('[type="submit"]');
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.classList.add('htmx-request');
}
}
});
/**
* Remove global loading class when request completes
*/
document.body.addEventListener('htmx:afterRequest', (evt) => {
activeRequests--;
if (activeRequests <= 0) {
activeRequests = 0;
document.body.classList.remove('htmx-loading');
}
// Re-enable submit buttons
const target = evt.target;
if (target.tagName === 'FORM' || target.closest('form')) {
const form = target.tagName === 'FORM' ? target : target.closest('form');
const submitBtn = form.querySelector('[type="submit"]');
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.classList.remove('htmx-request');
}
}
});
/**
* Handle search inputs with loading states
* Automatically adds loading indicator to search inputs during HTMX requests
*/
document.querySelectorAll('input[type="search"], input[data-search]').forEach(input => {
const wrapper = input.closest('.search-wrapper, .relative');
if (!wrapper) return;
// Create loading indicator if it doesn't exist
let indicator = wrapper.querySelector('.search-loading');
if (!indicator) {
indicator = document.createElement('span');
indicator.className = 'search-loading htmx-indicator absolute right-3 top-1/2 -translate-y-1/2';
indicator.innerHTML = '<i class="fas fa-spinner fa-spin text-muted-foreground"></i>';
wrapper.appendChild(indicator);
}
});
/**
* Swap skeleton with content utility
* Use data-skeleton-target to specify which skeleton to hide when content loads
*/
document.body.addEventListener('htmx:afterSwap', (evt) => {
const skeletonTarget = evt.target.dataset.skeletonTarget;
if (skeletonTarget) {
const skeleton = document.querySelector(skeletonTarget);
if (skeleton) {
skeleton.style.display = 'none';
}
}
});
});
/**
* Utility function to show skeleton and trigger HTMX load
* @param {string} targetId - ID of the target element
* @param {string} skeletonId - ID of the skeleton element
*/
window.showSkeletonAndLoad = function(targetId, skeletonId) {
const target = document.getElementById(targetId);
const skeleton = document.getElementById(skeletonId);
if (skeleton) {
skeleton.style.display = 'block';
}
if (target) {
htmx.trigger(target, 'load');
}
};
/**
* Utility function to replace content with skeleton during reload
* @param {string} targetId - ID of the target element
* @param {string} skeletonHtml - HTML string of skeleton to show
*/
window.reloadWithSkeleton = function(targetId, skeletonHtml) {
const target = document.getElementById(targetId);
if (target && skeletonHtml) {
// Store original content temporarily
target.dataset.originalContent = target.innerHTML;
target.innerHTML = skeletonHtml;
htmx.trigger(target, 'load');
}
};