diff --git a/index.js b/index.js index 56b6716..5d771fa 100644 --- a/index.js +++ b/index.js @@ -651,22 +651,174 @@ async function sendPlotProgression(type) { $('#rpg-plot-random, #rpg-plot-natural').prop('disabled', false).css('opacity', '1'); }, 1000); } -}/** +} + +/** + * Modern DiceModal ES6 Class + * Manages dice roller modal with proper state management and CSS classes + */ +class DiceModal { + constructor() { + this.modal = document.getElementById('rpg-dice-popup'); + this.animation = document.getElementById('rpg-dice-animation'); + this.result = document.getElementById('rpg-dice-result'); + this.resultValue = document.getElementById('rpg-dice-result-value'); + this.resultDetails = document.getElementById('rpg-dice-result-details'); + this.rollBtn = document.getElementById('rpg-dice-roll-btn'); + + this.state = 'IDLE'; // IDLE, ROLLING, SHOWING_RESULT + this.isAnimating = false; + } + + /** + * Opens the modal with proper animation + */ + open() { + if (this.isAnimating) return; + + // Apply theme + const theme = extensionSettings.theme; + this.modal.setAttribute('data-theme', theme); + + // Apply custom theme if needed + if (theme === 'custom') { + this._applyCustomTheme(); + } + + // Reset to initial state + this._setState('IDLE'); + + // Open modal with CSS class + this.modal.classList.add('is-open'); + this.modal.classList.remove('is-closing'); + + // Focus management + this.modal.querySelector('#rpg-dice-popup-close')?.focus(); + } + + /** + * Closes the modal with animation + */ + close() { + if (this.isAnimating) return; + + this.isAnimating = true; + this.modal.classList.add('is-closing'); + this.modal.classList.remove('is-open'); + + // Wait for animation to complete + setTimeout(() => { + this.modal.classList.remove('is-closing'); + this.isAnimating = false; + + // Clear pending roll + pendingDiceRoll = null; + }, 200); + } + + /** + * Starts the rolling animation + */ + startRolling() { + this._setState('ROLLING'); + } + + /** + * Shows the result + * @param {number} total - The total roll value + * @param {Array} rolls - Individual roll values + */ + showResult(total, rolls) { + this._setState('SHOWING_RESULT'); + + // Update result values + this.resultValue.textContent = total; + this.resultValue.classList.add('is-animating'); + + // Remove animation class after it completes + setTimeout(() => { + this.resultValue.classList.remove('is-animating'); + }, 500); + + // Show details if multiple rolls + if (rolls && rolls.length > 1) { + this.resultDetails.textContent = `Rolls: ${rolls.join(', ')}`; + } else { + this.resultDetails.textContent = ''; + } + } + + /** + * Manages modal state changes + * @private + */ + _setState(newState) { + this.state = newState; + + switch (newState) { + case 'IDLE': + this.rollBtn.hidden = false; + this.animation.hidden = true; + this.result.hidden = true; + break; + + case 'ROLLING': + this.rollBtn.hidden = true; + this.animation.hidden = false; + this.result.hidden = true; + this.animation.setAttribute('aria-busy', 'true'); + break; + + case 'SHOWING_RESULT': + this.rollBtn.hidden = true; + this.animation.hidden = true; + this.result.hidden = false; + this.animation.setAttribute('aria-busy', 'false'); + break; + } + } + + /** + * Applies custom theme colors + * @private + */ + _applyCustomTheme() { + const content = this.modal.querySelector('.rpg-dice-popup-content'); + if (content && extensionSettings.customColors) { + content.style.setProperty('--rpg-bg', extensionSettings.customColors.bg); + content.style.setProperty('--rpg-accent', extensionSettings.customColors.accent); + content.style.setProperty('--rpg-text', extensionSettings.customColors.text); + content.style.setProperty('--rpg-highlight', extensionSettings.customColors.highlight); + } + } +} + +// Global instance +let diceModal = null; + +/** * Sets up the dice roller functionality. */ function setupDiceRoller() { + // Initialize DiceModal instance + diceModal = new DiceModal(); // Click dice display to open popup $('#rpg-dice-display').on('click', function() { openDicePopup(); }); - // Close popup - $('#rpg-dice-popup-close, .rpg-dice-popup-overlay').on('click', function() { - // Discard pending roll without saving - pendingDiceRoll = null; + // Close popup - handle both close button and backdrop clicks + $('#rpg-dice-popup-close').on('click', function() { closeDicePopup(); }); + // Close on backdrop click (clicking outside content) + $('#rpg-dice-popup').on('click', function(e) { + if (e.target === this) { + closeDicePopup(); + } + }); + // Roll dice button $('#rpg-dice-roll-btn').on('click', async function() { await rollDice(); @@ -709,54 +861,45 @@ function clearDiceRoll() { /** * Opens the dice rolling popup. + * Backwards compatible wrapper for DiceModal class. */ function openDicePopup() { - // Apply current theme to popup - const theme = extensionSettings.theme; - $('#rpg-dice-popup').attr('data-theme', theme); - - $('#rpg-dice-popup').fadeIn(200); - $('#rpg-dice-animation').hide(); - $('#rpg-dice-result').hide(); - $('#rpg-dice-roll-btn').show(); - - // Apply custom theme if selected - if (theme === 'custom') { - applyCustomThemeToPopup(); + if (diceModal) { + diceModal.open(); } } /** - * Applies custom theme colors to the dice popup. + * Closes the dice rolling popup. + * Backwards compatible wrapper for DiceModal class. */ -function applyCustomThemeToPopup() { - const $popup = $('#rpg-dice-popup'); - $popup.find('.rpg-dice-popup-content').css({ - '--rpg-bg': extensionSettings.customColors.bg, - '--rpg-accent': extensionSettings.customColors.accent, - '--rpg-text': extensionSettings.customColors.text, - '--rpg-highlight': extensionSettings.customColors.highlight - }); +function closeDicePopup() { + if (diceModal) { + diceModal.close(); + } } /** - * Closes the dice rolling popup. + * @deprecated Legacy function - use diceModal._applyCustomTheme() instead */ -function closeDicePopup() { - $('#rpg-dice-popup').fadeOut(200); +function applyCustomThemeToPopup() { + if (diceModal) { + diceModal._applyCustomTheme(); + } } /** * Rolls the dice and displays result. + * Refactored to use DiceModal class. */ async function rollDice() { + if (!diceModal) return; + const count = parseInt(String($('#rpg-dice-count').val())) || 1; const sides = parseInt(String($('#rpg-dice-sides').val())) || 20; - // Hide roll button and show animation - $('#rpg-dice-roll-btn').hide(); - $('#rpg-dice-animation').show(); - $('#rpg-dice-result').hide(); + // Start rolling animation + diceModal.startRolling(); // Wait for animation (simulate rolling) await new Promise(resolve => setTimeout(resolve, 1200)); @@ -777,16 +920,8 @@ async function rollDice() { timestamp: Date.now() }; - // Hide animation and show result - $('#rpg-dice-animation').hide(); - $('#rpg-dice-result').show(); - $('#rpg-dice-result-value').text(total); - - if (rolls.length > 1) { - $('#rpg-dice-result-details').text(`Rolls: ${rolls.join(', ')}`); - } else { - $('#rpg-dice-result-details').text(''); - } + // Show result + diceModal.showResult(total, rolls); // Don't update sidebar display yet - only update when user clicks "Save Roll" } diff --git a/style.css b/style.css index 13fdfdd..70f3d77 100644 --- a/style.css +++ b/style.css @@ -2115,47 +2115,299 @@ body:has(.rpg-panel.rpg-position-left) #sheld { .rpg-stat-row:nth-child(5) { animation-delay: 0.3s; } /* ============================================ - DICE ROLL POPUP + DICE ROLL MODAL - MOBILE FIRST ============================================ */ + +/* CSS Custom Properties for Responsive Scaling */ +.rpg-dice-popup { + /* Fluid spacing that scales with viewport */ + --modal-padding: clamp(0.5rem, 2vw, 0.75rem); + --modal-gap: clamp(0.375rem, 1.5vw, 0.5rem); + --modal-border-width: 2px; + + /* Fluid typography */ + --modal-font-base: clamp(0.8rem, 3vw, 0.9rem); + --modal-font-small: clamp(0.7rem, 2.5vw, 0.8rem); + --modal-font-large: clamp(1.25rem, 6vw, 1.75rem); + --modal-font-huge: clamp(1.5rem, 8vw, 2.5rem); + + /* Touch-friendly sizing */ + --modal-button-height: 44px; + --modal-input-height: 44px; + + /* Content constraints - MUCH more conservative */ + --modal-max-width: min(90vw, 360px); + --modal-max-height: 70vh; +} + +/* Modal Container - Hidden by default */ .rpg-dice-popup { position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; + inset: 0; z-index: 10000; + display: none; + align-items: center; + justify-content: center; + padding: 0.5rem; +} + +/* Open state - managed by JavaScript classList */ +.rpg-dice-popup.is-open { + display: flex; + animation: fadeIn 0.2s ease-out; +} + +/* Closing state - allows exit animation */ +.rpg-dice-popup.is-closing { + display: flex; + animation: fadeOut 0.2s ease-in; +} + +/* Backdrop overlay - using ::before pseudo-element */ +.rpg-dice-popup::before { + content: ''; + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.85); + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); +} + +/* Modal Content Box */ +.rpg-dice-popup-content { + position: relative; + width: 100%; + max-width: var(--modal-max-width); + height: auto; + max-height: var(--modal-max-height); + min-height: 0; + background: rgba(30, 30, 30, 0.8); + border: var(--modal-border-width) solid var(--rpg-border); + border-radius: 0.5rem; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.9); + color: var(--rpg-text); + display: flex; + flex-direction: column; + overflow: hidden; + animation: slideInUp 0.3s cubic-bezier(0.16, 1, 0.3, 1); + margin: auto 0; +} + +/* Header */ +.rpg-dice-popup-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--modal-padding); + background: var(--rpg-accent); + border-bottom: var(--modal-border-width) solid var(--rpg-border); + flex-shrink: 0; +} + +.rpg-dice-popup-header h3 { + margin: 0; + font-size: var(--modal-font-base); + color: var(--rpg-highlight); + display: flex; + align-items: center; + gap: var(--modal-gap); +} + +/* Close button - touch-friendly */ +#rpg-dice-popup-close { + min-width: 44px; + min-height: 44px; + padding: 0.5rem; display: flex; align-items: center; justify-content: center; } -.rpg-dice-popup-overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.7); - backdrop-filter: blur(5px); +/* Scrollable Body */ +.rpg-dice-popup-body { + padding: var(--modal-padding); + overflow-y: auto; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; + flex: 1 1 auto; + min-height: 0; } -.rpg-dice-popup-content { - position: relative; - width: 90%; - max-width: 31.25rem; - background: var(--rpg-bg); - border: 3px solid var(--rpg-border); - border-radius: 0.938em; - box-shadow: 0 10px 50px rgba(0, 0, 0, 0.9); - overflow: hidden; - animation: popupSlideIn 0.3s ease-out; +/* Input Container */ +.rpg-dice-selector-container { + padding: var(--modal-padding); + background: rgba(0, 0, 0, 0.3); + border-radius: 0.5rem; + border: var(--modal-border-width) solid var(--rpg-border); + margin-bottom: var(--modal-gap); +} + +/* Input Grid - Stacked on mobile */ +.rpg-dice-selector { + display: grid; + grid-template-columns: 1fr; + gap: var(--modal-gap); + margin-bottom: var(--modal-gap); +} + +.rpg-dice-input-group { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.rpg-dice-input-group label { + font-size: var(--modal-font-small); + font-weight: 600; color: var(--rpg-text); } -@keyframes popupSlideIn { +.rpg-dice-input-group input, +.rpg-dice-input-group select { + width: 100%; + min-height: var(--modal-input-height); + padding: 0.5rem; + border: var(--modal-border-width) solid var(--rpg-border); + border-radius: 0.375rem; + background: var(--rpg-accent); + color: var(--rpg-text); + font-size: var(--modal-font-base); + font-weight: 600; + text-align: center; + transition: all 0.2s ease; +} + +.rpg-dice-input-group input:focus, +.rpg-dice-input-group select:focus { + outline: none; + border-color: var(--rpg-highlight); + box-shadow: 0 0 0 3px rgba(var(--rpg-highlight-rgb, 255, 0, 100), 0.2); + background: rgba(0, 0, 0, 0.5); +} + +/* Roll Button - touch-friendly */ +#rpg-dice-roll-btn { + width: 100%; + min-height: var(--modal-button-height); + padding: 0.75rem 1rem; + background: linear-gradient(135deg, var(--rpg-highlight), var(--rpg-accent)); + border: var(--modal-border-width) solid var(--rpg-highlight); + border-radius: 0.5rem; + color: var(--rpg-text); + font-size: var(--modal-font-base); + font-weight: 700; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +#rpg-dice-roll-btn:active { + transform: scale(0.98); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); +} + +/* Animation Section */ +.rpg-dice-animation { + text-align: center; + padding: var(--modal-padding); +} + +.rpg-dice-rolling i { + font-size: var(--modal-font-large); + color: var(--rpg-highlight); + animation: diceRoll 0.8s ease-in-out infinite; +} + +.rpg-dice-rolling-text { + margin-top: var(--modal-gap); + font-size: var(--modal-font-base); + font-weight: 600; + color: var(--rpg-highlight); + animation: pulseGlow 1s ease-in-out infinite; +} + +/* Result Section */ +.rpg-dice-result { + text-align: center; + padding: var(--modal-padding); + background: rgba(0, 0, 0, 0.3); + border-radius: 0.5rem; + border: var(--modal-border-width) solid var(--rpg-border); +} + +.rpg-dice-result-label { + font-size: var(--modal-font-small); + color: var(--rpg-text); + margin-bottom: 0.5rem; + text-transform: uppercase; + letter-spacing: 0.05em; + opacity: 0.8; +} + +.rpg-dice-result-value { + font-size: var(--modal-font-huge); + font-weight: 700; + color: var(--rpg-highlight); + text-shadow: 0 0 20px var(--rpg-highlight); + line-height: 1; +} + +.rpg-dice-result-value.is-animating { + animation: resultPop 0.5s cubic-bezier(0.16, 1, 0.3, 1); +} + +.rpg-dice-result-details { + margin-top: var(--modal-gap); + font-size: var(--modal-font-small); + color: var(--rpg-text); + opacity: 0.7; +} + +/* Save Button */ +.rpg-dice-save-btn { + margin-top: var(--modal-gap); + width: 100%; + min-height: var(--modal-button-height); + padding: 0.75rem 1rem; + background: linear-gradient(135deg, #28a745, #20c997); + border: var(--modal-border-width) solid #28a745; + border-radius: 0.5rem; + color: white; + font-size: var(--modal-font-base); + font-weight: 700; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 4px 12px rgba(40, 167, 69, 0.4); + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +.rpg-dice-save-btn:active { + transform: scale(0.98); + box-shadow: 0 2px 8px rgba(40, 167, 69, 0.4); +} + +/* Animations */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes fadeOut { + from { opacity: 1; } + to { opacity: 0; } +} + +@keyframes slideInUp { from { opacity: 0; - transform: translateY(-3.125rem) scale(0.9); + transform: translateY(20px) scale(0.95); } to { opacity: 1; @@ -2163,216 +2415,20 @@ body:has(.rpg-panel.rpg-position-left) #sheld { } } -.rpg-dice-popup-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1.25em; - background: var(--rpg-accent); - border-bottom: 2px solid var(--rpg-border); -} - -.rpg-dice-popup-header h3 { - margin: 0; - font-size: 1.25rem; - color: var(--rpg-highlight); - display: flex; - align-items: center; - gap: 0.625em; -} - -.rpg-dice-popup-body { - padding: 1.562em; -} - -.rpg-dice-selector-container { - padding: 1.25em; - background: rgba(0, 0, 0, 0.3); - border-radius: 0.75em; - border: 2px solid var(--rpg-border); - margin-bottom: 1.25em; -} - -.rpg-dice-selector { - display: flex; - gap: 0.938em; - margin-bottom: 0.938em; -} - -.rpg-dice-input-group { - flex: 1; -} - -.rpg-dice-input-group label { - display: block; - margin-bottom: 0.5em; - font-size: 0.812rem; - font-weight: bold; - color: var(--rpg-text); -} - -.rpg-dice-input-group input, -.rpg-dice-input-group select { - width: 100%; - padding: 0.625em; - border: 2px solid var(--rpg-border); - border-radius: 0.5em; - background: var(--rpg-accent); - color: var(--rpg-text); - font-size: 1rem; - font-weight: bold; - text-align: center; - transition: all 0.3s ease; -} - -.rpg-dice-input-group input:focus, -.rpg-dice-input-group select:focus { - outline: none; - border-color: var(--rpg-highlight); - box-shadow: 0 0 10px var(--rpg-highlight); - background: rgba(0, 0, 0, 0.5); -} - -#rpg-dice-roll-btn { - width: 100%; - padding: 0.75em 1.25em; - background: linear-gradient(135deg, var(--rpg-highlight), var(--rpg-accent)); - border: 2px solid var(--rpg-highlight); - border-radius: 0.625em; - color: var(--rpg-text); - font-size: 1rem; - font-weight: bold; - cursor: pointer; - transition: all 0.3s ease; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4); - display: flex; - align-items: center; - justify-content: center; - gap: 0.625em; -} - -#rpg-dice-roll-btn:hover { - transform: translateY(-0.125rem); - box-shadow: 0 6px 20px var(--rpg-highlight); - background: var(--rpg-highlight); -} - -#rpg-dice-roll-btn:active { - transform: translateY(0); - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4); -} - -.rpg-dice-animation { - text-align: center; - padding: 2.5em 1.25em; -} - -.rpg-dice-rolling i { - font-size: 5rem; - color: var(--rpg-highlight); - animation: diceRoll 0.8s ease-in-out infinite; -} - @keyframes diceRoll { - 0%, 100% { - transform: rotate(0deg) scale(1); - } - 25% { - transform: rotate(90deg) scale(1.1); - } - 50% { - transform: rotate(180deg) scale(1); - } - 75% { - transform: rotate(270deg) scale(1.1); - } -} - -.rpg-dice-rolling-text { - margin-top: 1.25em; - font-size: 1.125rem; - font-weight: bold; - color: var(--rpg-highlight); - animation: pulseGlow 1s ease-in-out infinite; -} - -.rpg-dice-result { - text-align: center; - padding: 1.875em 1.25em; - background: rgba(0, 0, 0, 0.3); - border-radius: 0.75em; - border: 2px solid var(--rpg-border); -} - -.rpg-dice-result-label { - font-size: 0.875rem; - color: var(--rpg-text); - margin-bottom: 0.625em; - text-transform: uppercase; - letter-spacing: 0.062em; -} - -.rpg-dice-result-value { - font-size: 3.75rem; - font-weight: bold; - color: var(--rpg-highlight); - text-shadow: 0 0 20px var(--rpg-highlight); - animation: resultPop 0.5s ease-out; + 0%, 100% { transform: rotate(0deg); } + 25% { transform: rotate(90deg) scale(1.1); } + 50% { transform: rotate(180deg); } + 75% { transform: rotate(270deg) scale(1.1); } } @keyframes resultPop { - 0% { - transform: scale(0); - opacity: 0; - } - 50% { - transform: scale(1.2); - } - 100% { - transform: scale(1); - opacity: 1; - } + 0% { transform: scale(0); opacity: 0; } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); opacity: 1; } } -.rpg-dice-result-details { - margin-top: 0.938em; - font-size: 0.875rem; - color: var(--rpg-text); - opacity: 0.8; -} - -.rpg-dice-save-btn { - margin-top: 1.25em; - width: 100%; - max-width: 12.5rem; - padding: 0.625em 1.25em; - background: linear-gradient(135deg, #28a745, #20c997); - border: 2px solid #28a745; - border-radius: 0.625em; - color: white; - font-size: 0.938rem; - font-weight: bold; - cursor: pointer; - transition: all 0.3s ease; - box-shadow: 0 4px 15px rgba(40, 167, 69, 0.4); - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.5em; -} - -.rpg-dice-save-btn:hover { - transform: translateY(-0.125rem); - box-shadow: 0 6px 20px rgba(40, 167, 69, 0.6); - background: #28a745; -} - -.rpg-dice-save-btn:active { - transform: translateY(0); - box-shadow: 0 2px 10px rgba(40, 167, 69, 0.4); -} - -/* Theme-specific popup styles */ +/* Theme Support - CSS Custom Properties */ .rpg-dice-popup[data-theme="sci-fi"] .rpg-dice-popup-content { --rpg-bg: #0a0e27; --rpg-accent: #1a1f3a; @@ -2397,6 +2453,35 @@ body:has(.rpg-panel.rpg-position-left) #sheld { --rpg-border: #ff00ff; } +/* Desktop Enhancement (1001px+) */ +@media (min-width: 1001px) { + .rpg-dice-popup { + --modal-padding: 1.5rem; + --modal-gap: 1rem; + --modal-font-base: 1rem; + --modal-font-small: 0.875rem; + --modal-font-large: 3rem; + --modal-font-huge: 3.75rem; + --modal-max-width: 500px; + } + + /* Side-by-side inputs on desktop */ + .rpg-dice-selector { + grid-template-columns: 1fr 1fr; + } + + /* Hover effects on desktop */ + #rpg-dice-roll-btn:hover, + .rpg-dice-save-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px currentColor; + } + + .rpg-dice-save-btn { + max-width: 200px; + } +} + /* ============================================ HTML PROMPT TOGGLE ============================================ */ diff --git a/template.html b/template.html index 3815c90..4ba4257 100644 --- a/template.html +++ b/template.html @@ -211,27 +211,29 @@ - -