diff --git a/.gitignore b/.gitignore index 0beb2dc..789b14f 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,9 @@ # Node modules (if any) node_modules/ + +# Environment variables +.env + +# Claude +CLAUDE.md \ No newline at end of file diff --git a/index.js b/index.js index 7d53c5b..b75cc5e 100644 --- a/index.js +++ b/index.js @@ -35,6 +35,10 @@ let extensionSettings = { statBarColorLow: '#cc3333', // Color for low stat values (red) statBarColorHigh: '#33cc66', // Color for high stat values (green) enableAnimations: true, // Enable smooth animations for stats and content updates + mobileFabPosition: { + top: 'calc(var(--topBarBlockSize) + 60px)', + right: '12px' + }, // Saved position for mobile FAB button userStats: { health: 100, sustenance: 100, @@ -511,9 +515,12 @@ async function initUI() { renderThoughts(); updateDiceDisplay(); setupDiceRoller(); + setupClassicStatsButtons(); setupSettingsPopup(); addDiceQuickReply(); setupPlotButtons(); + setupMobileKeyboardHandling(); + setupContentEditableScrolling(); } /** @@ -647,22 +654,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(); @@ -705,54 +864,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)); @@ -773,16 +923,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" } @@ -840,60 +982,173 @@ function addDiceQuickReply() { } /** - * Opens the settings popup. + * Sets up event listeners for classic stat +/- buttons using delegation. + * Uses delegated events to persist across re-renders of the stats section. */ -function openSettingsPopup() { - const theme = extensionSettings.theme || 'default'; - $('#rpg-settings-popup').attr('data-theme', theme); +function setupClassicStatsButtons() { + if (!$userStatsContainer) return; - // Apply custom theme colors if custom theme is selected - if (theme === 'custom') { - applyCustomThemeToSettingsPopup(); - } + // Delegated event listener for increase buttons + $userStatsContainer.on('click', '.rpg-stat-increase', function() { + const stat = $(this).data('stat'); + if (extensionSettings.classicStats[stat] < 100) { + extensionSettings.classicStats[stat]++; + saveSettings(); + saveChatData(); + // Update only the specific stat value, not the entire stats panel + $(this).closest('.rpg-classic-stat').find('.rpg-classic-stat-value').text(extensionSettings.classicStats[stat]); + } + }); - $('#rpg-settings-popup').fadeIn(200); -} - -/** - * Closes the settings popup. - */ -function closeSettingsPopup() { - $('#rpg-settings-popup').fadeOut(200); -} - -/** - * Applies custom theme colors to the settings popup. - */ -function applyCustomThemeToSettingsPopup() { - const popup = $('#rpg-settings-popup .rpg-settings-popup-content'); - popup.css({ - '--rpg-bg': extensionSettings.customColors.bg, - '--rpg-accent': extensionSettings.customColors.accent, - '--rpg-text': extensionSettings.customColors.text, - '--rpg-highlight': extensionSettings.customColors.highlight + // Delegated event listener for decrease buttons + $userStatsContainer.on('click', '.rpg-stat-decrease', function() { + const stat = $(this).data('stat'); + if (extensionSettings.classicStats[stat] > 1) { + extensionSettings.classicStats[stat]--; + saveSettings(); + saveChatData(); + // Update only the specific stat value, not the entire stats panel + $(this).closest('.rpg-classic-stat').find('.rpg-classic-stat-value').text(extensionSettings.classicStats[stat]); + } }); } +/** + * SettingsModal - Manages the settings popup modal + * Handles opening, closing, theming, and animations + */ +class SettingsModal { + constructor() { + this.modal = document.getElementById('rpg-settings-popup'); + this.content = this.modal?.querySelector('.rpg-settings-popup-content'); + this.isAnimating = false; + } + + /** + * Opens the modal with proper animation + */ + open() { + if (this.isAnimating || !this.modal) return; + + // Apply theme + const theme = extensionSettings.theme || 'default'; + this.modal.setAttribute('data-theme', theme); + + // Apply custom theme if needed + if (theme === 'custom') { + this._applyCustomTheme(); + } + + // Open modal with CSS class + this.modal.classList.add('is-open'); + this.modal.classList.remove('is-closing'); + + // Focus management + this.modal.querySelector('#rpg-close-settings')?.focus(); + } + + /** + * Closes the modal with animation + */ + close() { + if (this.isAnimating || !this.modal) 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; + }, 200); + } + + /** + * Updates the theme in real-time (used when theme selector changes) + */ + updateTheme() { + if (!this.modal) return; + + const theme = extensionSettings.theme || 'default'; + this.modal.setAttribute('data-theme', theme); + + if (theme === 'custom') { + this._applyCustomTheme(); + } else { + // Clear custom CSS variables to let theme CSS take over + this._clearCustomTheme(); + } + } + + /** + * Applies custom theme colors + * @private + */ + _applyCustomTheme() { + if (!this.content || !extensionSettings.customColors) return; + + this.content.style.setProperty('--rpg-bg', extensionSettings.customColors.bg); + this.content.style.setProperty('--rpg-accent', extensionSettings.customColors.accent); + this.content.style.setProperty('--rpg-text', extensionSettings.customColors.text); + this.content.style.setProperty('--rpg-highlight', extensionSettings.customColors.highlight); + } + + /** + * Clears custom theme colors + * @private + */ + _clearCustomTheme() { + if (!this.content) return; + + this.content.style.setProperty('--rpg-bg', ''); + this.content.style.setProperty('--rpg-accent', ''); + this.content.style.setProperty('--rpg-text', ''); + this.content.style.setProperty('--rpg-highlight', ''); + } +} + +// Global instance +let settingsModal = null; + +/** + * Opens the settings popup. + * Backwards compatible wrapper for SettingsModal class. + */ +function openSettingsPopup() { + if (settingsModal) { + settingsModal.open(); + } +} + +/** + * Closes the settings popup. + * Backwards compatible wrapper for SettingsModal class. + */ +function closeSettingsPopup() { + if (settingsModal) { + settingsModal.close(); + } +} + +/** + * Applies custom theme colors to the settings popup. + * Backwards compatible wrapper for SettingsModal class. + * @deprecated Use settingsModal.updateTheme() instead + */ +function applyCustomThemeToSettingsPopup() { + if (settingsModal) { + settingsModal._applyCustomTheme(); + } +} + /** * Updates the settings popup theme in real-time. + * Backwards compatible wrapper for SettingsModal class. */ function updateSettingsPopupTheme() { - const theme = extensionSettings.theme || 'default'; - const popup = $('#rpg-settings-popup .rpg-settings-popup-content'); - - $('#rpg-settings-popup').attr('data-theme', theme); - - // Apply custom theme colors if custom theme is selected - if (theme === 'custom') { - applyCustomThemeToSettingsPopup(); - } else { - // Clear custom CSS variables to let theme CSS take over - popup.css({ - '--rpg-bg': '', - '--rpg-accent': '', - '--rpg-text': '', - '--rpg-highlight': '' - }); + if (settingsModal) { + settingsModal.updateTheme(); } } @@ -901,16 +1156,26 @@ function updateSettingsPopupTheme() { * Sets up the settings popup functionality. */ function setupSettingsPopup() { + // Initialize SettingsModal instance + settingsModal = new SettingsModal(); + // Open settings popup $('#rpg-open-settings').on('click', function() { openSettingsPopup(); }); - // Close settings popup - $('#rpg-close-settings, .rpg-settings-popup-overlay').on('click', function() { + // Close settings popup - close button + $('#rpg-close-settings').on('click', function() { closeSettingsPopup(); }); + // Close on backdrop click (clicking outside content) + $('#rpg-settings-popup').on('click', function(e) { + if (e.target === this) { + closeSettingsPopup(); + } + }); + // Clear cache button $('#rpg-clear-cache').on('click', function() { // Clear the data @@ -981,6 +1246,24 @@ function setupSettingsPopup() { }); } +/** + * Helper function to close the mobile panel with animation. + */ +function closeMobilePanelWithAnimation() { + const $panel = $('#rpg-companion-panel'); + const $mobileToggle = $('#rpg-mobile-toggle'); + + // Add closing class to trigger slide-out animation + $panel.removeClass('rpg-mobile-open').addClass('rpg-mobile-closing'); + $mobileToggle.removeClass('active'); + + // Wait for animation to complete before hiding + $panel.one('animationend', function() { + $panel.removeClass('rpg-mobile-closing'); + $('.rpg-mobile-overlay').remove(); + }); +} + /** * Sets up the mobile toggle button (FAB). */ @@ -989,27 +1272,681 @@ function setupMobileToggle() { const $panel = $('#rpg-companion-panel'); const $overlay = $('
'); - // Toggle panel visibility on mobile - $mobileToggle.on('click', function() { - if ($panel.hasClass('rpg-mobile-open')) { - // Close panel - $panel.removeClass('rpg-mobile-open'); - $overlay.remove(); - $mobileToggle.removeClass('active'); + // DIAGNOSTIC: Check if elements exist and log setup state + console.log('[RPG Mobile] ========================================'); + console.log('[RPG Mobile] setupMobileToggle called'); + console.log('[RPG Mobile] Button exists:', $mobileToggle.length > 0, 'jQuery object:', $mobileToggle); + console.log('[RPG Mobile] Panel exists:', $panel.length > 0); + console.log('[RPG Mobile] Window width:', window.innerWidth); + console.log('[RPG Mobile] Is mobile viewport (<=1000):', window.innerWidth <= 1000); + console.log('[RPG Mobile] ========================================'); + + if ($mobileToggle.length === 0) { + console.error('[RPG Mobile] ERROR: Mobile toggle button not found in DOM!'); + console.error('[RPG Mobile] Cannot attach event handlers - button does not exist'); + return; // Exit early if button doesn't exist + } + + // Load and apply saved FAB position + if (extensionSettings.mobileFabPosition) { + const pos = extensionSettings.mobileFabPosition; + console.log('[RPG Mobile] Loading saved FAB position:', pos); + + // Apply saved position + if (pos.top) $mobileToggle.css('top', pos.top); + if (pos.right) $mobileToggle.css('right', pos.right); + if (pos.bottom) $mobileToggle.css('bottom', pos.bottom); + if (pos.left) $mobileToggle.css('left', pos.left); + + // Constrain to viewport after position is applied + requestAnimationFrame(() => constrainFabToViewport()); + } + + // Touch/drag state + let isDragging = false; + let touchStartTime = 0; + let touchStartX = 0; + let touchStartY = 0; + let buttonStartX = 0; + let buttonStartY = 0; + const LONG_PRESS_DURATION = 200; // ms to hold before enabling drag + const MOVE_THRESHOLD = 10; // px to move before enabling drag + let rafId = null; // RequestAnimationFrame ID for smooth updates + let pendingX = null; + let pendingY = null; + + // Update position using requestAnimationFrame for smooth rendering + function updateFabPosition() { + if (pendingX !== null && pendingY !== null) { + $mobileToggle.css({ + left: pendingX + 'px', + top: pendingY + 'px', + right: 'auto', + bottom: 'auto' + }); + pendingX = null; + pendingY = null; + } + rafId = null; + } + + // Touch start - begin tracking + $mobileToggle.on('touchstart', function(e) { + const touch = e.originalEvent.touches[0]; + + touchStartTime = Date.now(); + touchStartX = touch.clientX; + touchStartY = touch.clientY; + + const offset = $mobileToggle.offset(); + buttonStartX = offset.left; + buttonStartY = offset.top; + + isDragging = false; + }); + + // Touch move - check if should start dragging + $mobileToggle.on('touchmove', function(e) { + const touch = e.originalEvent.touches[0]; + const deltaX = touch.clientX - touchStartX; + const deltaY = touch.clientY - touchStartY; + const timeSinceStart = Date.now() - touchStartTime; + const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + + // Start dragging if held long enough OR moved far enough + if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) { + isDragging = true; + $mobileToggle.addClass('dragging'); // Disable transitions while dragging + } + + if (isDragging) { + e.preventDefault(); // Prevent scrolling while dragging + + // Calculate new position + let newX = buttonStartX + deltaX; + let newY = buttonStartY + deltaY; + + // Get button dimensions + const buttonWidth = $mobileToggle.outerWidth(); + const buttonHeight = $mobileToggle.outerHeight(); + + // Constrain to viewport with 10px padding + const minX = 10; + const maxX = window.innerWidth - buttonWidth - 10; + const minY = 10; + const maxY = window.innerHeight - buttonHeight - 10; + + newX = Math.max(minX, Math.min(maxX, newX)); + newY = Math.max(minY, Math.min(maxY, newY)); + + // Store pending position and request animation frame for smooth update + pendingX = newX; + pendingY = newY; + if (!rafId) { + rafId = requestAnimationFrame(updateFabPosition); + } + } + }); + + // Mouse drag support for desktop + let mouseDown = false; + + $mobileToggle.on('mousedown', function(e) { + // Prevent default to avoid text selection + e.preventDefault(); + + touchStartTime = Date.now(); + touchStartX = e.clientX; + touchStartY = e.clientY; + + const offset = $mobileToggle.offset(); + buttonStartX = offset.left; + buttonStartY = offset.top; + + isDragging = false; + mouseDown = true; + }); + + // Mouse move - only track if mouse is down + $(document).on('mousemove', function(e) { + if (!mouseDown) return; + + const deltaX = e.clientX - touchStartX; + const deltaY = e.clientY - touchStartY; + const timeSinceStart = Date.now() - touchStartTime; + const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + + // Start dragging if held long enough OR moved far enough + if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) { + isDragging = true; + $mobileToggle.addClass('dragging'); // Disable transitions while dragging + } + + if (isDragging) { + e.preventDefault(); + + // Calculate new position + let newX = buttonStartX + deltaX; + let newY = buttonStartY + deltaY; + + // Get button dimensions + const buttonWidth = $mobileToggle.outerWidth(); + const buttonHeight = $mobileToggle.outerHeight(); + + // Constrain to viewport with 10px padding + const minX = 10; + const maxX = window.innerWidth - buttonWidth - 10; + const minY = 10; + const maxY = window.innerHeight - buttonHeight - 10; + + newX = Math.max(minX, Math.min(maxX, newX)); + newY = Math.max(minY, Math.min(maxY, newY)); + + // Store pending position and request animation frame for smooth update + pendingX = newX; + pendingY = newY; + if (!rafId) { + rafId = requestAnimationFrame(updateFabPosition); + } + } + }); + + // Mouse up - save position or let click handler toggle + $(document).on('mouseup', function(e) { + if (!mouseDown) return; + + mouseDown = false; + + if (isDragging) { + // Was dragging - save new position + const offset = $mobileToggle.offset(); + const newPosition = { + left: offset.left + 'px', + top: offset.top + 'px' + }; + + extensionSettings.mobileFabPosition = newPosition; + saveSettings(); + + console.log('[RPG Mobile] Saved new FAB position (mouse):', newPosition); + + // Constrain to viewport bounds (now that position is saved) + setTimeout(() => constrainFabToViewport(), 10); + + // Re-enable transitions with smooth animation + setTimeout(() => { + $mobileToggle.removeClass('dragging'); + }, 50); + + isDragging = false; + + // Prevent click from firing after drag + e.preventDefault(); + e.stopPropagation(); + + // Add flag to prevent click handler from firing + $mobileToggle.data('just-dragged', true); + setTimeout(() => { + $mobileToggle.data('just-dragged', false); + }, 100); + } + // If not dragging, let the click handler toggle the panel + }); + + // Touch end - save position or toggle panel + $mobileToggle.on('touchend', function(e) { + // TEMPORARILY COMMENTED FOR DIAGNOSIS - might be blocking click fallback + // e.preventDefault(); + + if (isDragging) { + // Was dragging - save new position + const offset = $mobileToggle.offset(); + const newPosition = { + left: offset.left + 'px', + top: offset.top + 'px' + }; + + extensionSettings.mobileFabPosition = newPosition; + saveSettings(); + + console.log('[RPG Mobile] Saved new FAB position:', newPosition); + + // Constrain to viewport bounds (now that position is saved) + setTimeout(() => constrainFabToViewport(), 10); + + // Re-enable transitions with smooth animation + setTimeout(() => { + $mobileToggle.removeClass('dragging'); + }, 50); + + isDragging = false; } else { - // Open panel + // Was a tap - toggle panel + console.log('[RPG Mobile] Quick tap detected - toggling panel'); + + if ($panel.hasClass('rpg-mobile-open')) { + // Close panel with animation + closeMobilePanelWithAnimation(); + } else { + // Open panel + $panel.addClass('rpg-mobile-open'); + $('body').append($overlay); + $mobileToggle.addClass('active'); + + // Close when clicking overlay + $overlay.on('click', function() { + closeMobilePanelWithAnimation(); + }); + } + } + }); + + // Click handler - works on both mobile and desktop + $mobileToggle.on('click', function(e) { + // Skip if we just finished dragging + if ($mobileToggle.data('just-dragged')) { + console.log('[RPG Mobile] Click blocked - just finished dragging'); + return; + } + + console.log('[RPG Mobile] >>> CLICK EVENT FIRED <<<', { + windowWidth: window.innerWidth, + isMobileViewport: window.innerWidth <= 1000, + panelOpen: $panel.hasClass('rpg-mobile-open') + }); + + // Work on both mobile and desktop (removed viewport check) + if ($panel.hasClass('rpg-mobile-open')) { + console.log('[RPG Mobile] Click: Closing panel'); + closeMobilePanelWithAnimation(); + } else { + console.log('[RPG Mobile] Click: Opening panel'); $panel.addClass('rpg-mobile-open'); $('body').append($overlay); $mobileToggle.addClass('active'); - // Close when clicking overlay $overlay.on('click', function() { - $panel.removeClass('rpg-mobile-open'); - $overlay.remove(); - $mobileToggle.removeClass('active'); + console.log('[RPG Mobile] Overlay clicked - closing panel'); + closeMobilePanelWithAnimation(); }); } }); + + // Handle viewport resize to manage desktop/mobile transitions + let wasMobile = window.innerWidth <= 1000; + let resizeTimer; + + $(window).on('resize', function() { + clearTimeout(resizeTimer); + + const isMobile = window.innerWidth <= 1000; + const $panel = $('#rpg-companion-panel'); + const $mobileToggle = $('#rpg-mobile-toggle'); + + // Transitioning from desktop to mobile - handle immediately for smooth transition + if (!wasMobile && isMobile) { + console.log('[RPG Mobile] Transitioning desktop -> mobile'); + + // Remove desktop positioning classes + $panel.removeClass('rpg-position-right rpg-position-left rpg-position-top'); + + // Clear collapsed state - mobile doesn't use collapse + $panel.removeClass('rpg-collapsed'); + + // Close panel on mobile with animation + closeMobilePanelWithAnimation(); + + // Clear any inline styles that might be overriding CSS + $panel.attr('style', ''); + + console.log('[RPG Mobile] After cleanup:', { + panelClasses: $panel.attr('class'), + inlineStyles: $panel.attr('style'), + panelPosition: { + top: $panel.css('top'), + bottom: $panel.css('bottom'), + transform: $panel.css('transform'), + visibility: $panel.css('visibility') + } + }); + + // Set up mobile tabs IMMEDIATELY (no debounce delay) + setupMobileTabs(); + + // Update icon for mobile state + updateCollapseToggleIcon(); + + wasMobile = isMobile; + return; + } + + // For mobile to desktop transition, use debounce + resizeTimer = setTimeout(function() { + const isMobile = window.innerWidth <= 1000; + + // Transitioning from mobile to desktop + if (wasMobile && !isMobile) { + // Disable transitions to prevent left→right slide animation + $panel.css('transition', 'none'); + + $panel.removeClass('rpg-mobile-open rpg-mobile-closing'); + $mobileToggle.removeClass('active'); + $('.rpg-mobile-overlay').remove(); + + // Restore desktop positioning class + const position = extensionSettings.panelPosition || 'right'; + $panel.addClass('rpg-position-' + position); + + // Remove mobile tabs structure + removeMobileTabs(); + + // Force reflow to apply position instantly + $panel[0].offsetHeight; + + // Re-enable transitions after positioned + setTimeout(function() { + $panel.css('transition', ''); + }, 50); + } + + wasMobile = isMobile; + + // Constrain FAB to viewport after resize (only if user has positioned it) + constrainFabToViewport(); + }, 150); // Debounce only for mobile→desktop + }); + + // Initialize mobile tabs if starting on mobile + const isMobile = window.innerWidth <= 1000; + if (isMobile) { + const $panel = $('#rpg-companion-panel'); + // Clear any inline styles + $panel.attr('style', ''); + + console.log('[RPG Mobile] Initial load on mobile viewport:', { + panelClasses: $panel.attr('class'), + inlineStyles: $panel.attr('style'), + panelPosition: { + top: $panel.css('top'), + bottom: $panel.css('top'), + transform: $panel.css('transform'), + visibility: $panel.css('visibility') + } + }); + setupMobileTabs(); + // Set initial icon for mobile + updateCollapseToggleIcon(); + } +} + +/** + * Constrains the mobile FAB button to viewport bounds with top-bar awareness. + * Only runs when button is in user-controlled state (mobileFabPosition exists). + * Ensures button never goes behind the top bar or outside viewport edges. + */ +function constrainFabToViewport() { + // Only constrain if user has set a custom position + if (!extensionSettings.mobileFabPosition) { + console.log('[RPG Mobile] Skipping viewport constraint - using CSS defaults'); + return; + } + + const $mobileToggle = $('#rpg-mobile-toggle'); + if ($mobileToggle.length === 0) return; + + // Skip if button is not visible + if (!$mobileToggle.is(':visible')) { + console.log('[RPG Mobile] Skipping viewport constraint - button not visible'); + return; + } + + // Get current position + const offset = $mobileToggle.offset(); + if (!offset) return; + + let currentX = offset.left; + let currentY = offset.top; + + const buttonWidth = $mobileToggle.outerWidth(); + const buttonHeight = $mobileToggle.outerHeight(); + + // Get top bar height from CSS variable (fallback to 50px if not set) + const topBarHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--topBarBlockSize')) || 50; + + // Calculate viewport bounds with padding + // Use top bar height + extra padding for top bound + const minX = 10; + const maxX = window.innerWidth - buttonWidth - 10; + const minY = topBarHeight + 60; // Top bar + extra space for visibility + const maxY = window.innerHeight - buttonHeight - 10; + + // Constrain to bounds + let newX = Math.max(minX, Math.min(maxX, currentX)); + let newY = Math.max(minY, Math.min(maxY, currentY)); + + // Only update if position changed + if (newX !== currentX || newY !== currentY) { + console.log('[RPG Mobile] Constraining FAB to viewport:', { + old: { x: currentX, y: currentY }, + new: { x: newX, y: newY }, + viewport: { width: window.innerWidth, height: window.innerHeight }, + topBarHeight + }); + + // Apply new position + $mobileToggle.css({ + left: newX + 'px', + top: newY + 'px', + right: 'auto', + bottom: 'auto' + }); + + // Save corrected position + extensionSettings.mobileFabPosition = { + left: newX + 'px', + top: newY + 'px' + }; + saveSettings(); + } +} + +/** + * Sets up mobile tab navigation for organizing content. + * Only runs on mobile viewports (<=1000px). + */ +function setupMobileTabs() { + const isMobile = window.innerWidth <= 1000; + if (!isMobile) return; + + // Check if tabs already exist + if ($('.rpg-mobile-tabs').length > 0) return; + + const $panel = $('#rpg-companion-panel'); + const $contentBox = $panel.find('.rpg-content-box'); + + // Get existing sections + const $userStats = $('#rpg-user-stats'); + const $infoBox = $('#rpg-info-box'); + const $thoughts = $('#rpg-thoughts'); + + // If no sections exist, nothing to organize + if ($userStats.length === 0 && $infoBox.length === 0 && $thoughts.length === 0) { + return; + } + + // Create tab navigation (only show tabs for sections that exist) + const tabs = []; + const hasInfoOrCharacters = $infoBox.length > 0 || $thoughts.length > 0; + + if ($userStats.length > 0) { + tabs.push(''); + } + // Combine Info and Characters into one tab + if (hasInfoOrCharacters) { + tabs.push(''); + } + + const $tabNav = $('
' + tabs.join('') + '
'); + + // Determine which tab should be active + let firstTab = ''; + if ($userStats.length > 0) firstTab = 'stats'; + else if (hasInfoOrCharacters) firstTab = 'info-characters'; + + // Create tab content wrappers + const $statsTab = $('
'); + const $infoCharactersTab = $('
'); + + // Create combined content wrapper for Info and Characters + const $combinedWrapper = $('
'); + + // Move sections into their respective tabs (detach to preserve event handlers) + if ($userStats.length > 0) { + $statsTab.append($userStats.detach()); + $userStats.show(); + } + if ($infoBox.length > 0) { + $combinedWrapper.append($infoBox.detach()); + $infoBox.show(); + } + if ($thoughts.length > 0) { + $combinedWrapper.append($thoughts.detach()); + $thoughts.show(); + } + + // Add combined wrapper to the info-characters tab + if (hasInfoOrCharacters) { + $infoCharactersTab.append($combinedWrapper); + } + + // Hide dividers on mobile + $('.rpg-divider').hide(); + + // Build mobile tab structure + const $mobileContainer = $('
'); + $mobileContainer.append($tabNav); + + // Only append tab content wrappers that have content + if ($userStats.length > 0) $mobileContainer.append($statsTab); + if (hasInfoOrCharacters) $mobileContainer.append($infoCharactersTab); + + // Insert mobile tab structure at the beginning of content box + $contentBox.prepend($mobileContainer); + + // Handle tab switching + $tabNav.find('.rpg-mobile-tab').on('click', function() { + const tabName = $(this).data('tab'); + + // Update active tab button + $tabNav.find('.rpg-mobile-tab').removeClass('active'); + $(this).addClass('active'); + + // Update active tab content + $mobileContainer.find('.rpg-mobile-tab-content').removeClass('active'); + $mobileContainer.find('[data-tab-content="' + tabName + '"]').addClass('active'); + }); +} + +/** + * Removes mobile tab navigation and restores desktop layout. + */ +function removeMobileTabs() { + // Get sections from tabs before removing + const $userStats = $('#rpg-user-stats').detach(); + const $infoBox = $('#rpg-info-box').detach(); + const $thoughts = $('#rpg-thoughts').detach(); + + // Remove mobile tab container + $('.rpg-mobile-container').remove(); + + // Get dividers + const $dividerStats = $('#rpg-divider-stats'); + const $dividerInfo = $('#rpg-divider-info'); + + // Restore original sections to content box in correct order + const $contentBox = $('.rpg-content-box'); + + // Re-insert sections in original order + if ($dividerStats.length) { + $dividerStats.before($userStats); + $dividerInfo.before($infoBox); + $contentBox.append($thoughts); + } else { + // Fallback if dividers don't exist + $contentBox.prepend($thoughts); + $contentBox.prepend($infoBox); + $contentBox.prepend($userStats); + } + + // Show sections and dividers + $userStats.show(); + $infoBox.show(); + $thoughts.show(); + $('.rpg-divider').show(); +} + +/** + * Sets up mobile keyboard handling using Visual Viewport API. + * Prevents layout squashing when keyboard appears by detecting + * viewport changes and adding CSS classes for adjustment. + */ +function setupMobileKeyboardHandling() { + if (!window.visualViewport) { + // console.log('[RPG Mobile] Visual Viewport API not supported'); + return; + } + + const $panel = $('#rpg-companion-panel'); + let keyboardVisible = false; + + // Listen for viewport resize (keyboard show/hide) + window.visualViewport.addEventListener('resize', () => { + // Only handle if panel is open on mobile + if (!$panel.hasClass('rpg-mobile-open')) return; + + const viewportHeight = window.visualViewport.height; + const windowHeight = window.innerHeight; + + // Keyboard visible if viewport significantly smaller than window + // Using 75% threshold to account for browser UI variations + const isKeyboardShowing = viewportHeight < windowHeight * 0.75; + + if (isKeyboardShowing && !keyboardVisible) { + // Keyboard just appeared + keyboardVisible = true; + $panel.addClass('rpg-keyboard-visible'); + // console.log('[RPG Mobile] Keyboard opened'); + } else if (!isKeyboardShowing && keyboardVisible) { + // Keyboard just disappeared + keyboardVisible = false; + $panel.removeClass('rpg-keyboard-visible'); + // console.log('[RPG Mobile] Keyboard closed'); + } + }); +} + +/** + * Handles focus on contenteditable fields to ensure they're visible when keyboard appears. + * Uses smooth scrolling to bring focused field into view with proper padding. + */ +function setupContentEditableScrolling() { + const $panel = $('#rpg-companion-panel'); + + // Use event delegation for all contenteditable fields + $panel.on('focusin', '[contenteditable="true"]', function(e) { + const $field = $(this); + + // Small delay to let keyboard animate in + setTimeout(() => { + // Scroll field into view with padding + // Using 'center' to ensure field is in middle of viewport + $field[0].scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'nearest' + }); + }, 300); + }); } /** @@ -1020,7 +1957,79 @@ function setupCollapseToggle() { const $panel = $('#rpg-companion-panel'); const $icon = $collapseToggle.find('i'); - $collapseToggle.on('click', function() { + $collapseToggle.on('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + + const isMobile = window.innerWidth <= 1000; + + // On mobile: button toggles panel open/closed (same as desktop behavior) + if (isMobile) { + const isOpen = $panel.hasClass('rpg-mobile-open'); + console.log('[RPG Mobile] Collapse toggle clicked. Current state:', { + isOpen, + panelClasses: $panel.attr('class'), + inlineStyles: $panel.attr('style'), + panelPosition: { + top: $panel.css('top'), + bottom: $panel.css('bottom'), + transform: $panel.css('transform'), + visibility: $panel.css('visibility') + } + }); + + if (isOpen) { + // Close panel with animation + console.log('[RPG Mobile] Closing panel'); + closeMobilePanelWithAnimation(); + } else { + // Open panel + console.log('[RPG Mobile] Opening panel'); + $panel.addClass('rpg-mobile-open'); + const $overlay = $('
'); + $('body').append($overlay); + + // Debug: Check state after animation should complete + setTimeout(() => { + console.log('[RPG Mobile] 500ms after opening:', { + panelClasses: $panel.attr('class'), + hasOpenClass: $panel.hasClass('rpg-mobile-open'), + visibility: $panel.css('visibility'), + transform: $panel.css('transform'), + display: $panel.css('display'), + opacity: $panel.css('opacity') + }); + }, 500); + + // Close when clicking overlay + $overlay.on('click', function() { + console.log('[RPG Mobile] Overlay clicked - closing panel'); + closeMobilePanelWithAnimation(); + updateCollapseToggleIcon(); + }); + } + + // Update icon to reflect new state + updateCollapseToggleIcon(); + + console.log('[RPG Mobile] After toggle:', { + panelClasses: $panel.attr('class'), + inlineStyles: $panel.attr('style'), + panelPosition: { + top: $panel.css('top'), + bottom: $panel.css('bottom'), + transform: $panel.css('transform'), + visibility: $panel.css('visibility') + }, + gameContainer: { + opacity: $('.rpg-game-container').css('opacity'), + visibility: $('.rpg-game-container').css('visibility') + } + }); + return; + } + + // Desktop behavior: collapse/expand side panel const isCollapsed = $panel.hasClass('rpg-collapsed'); if (isCollapsed) { @@ -1057,21 +2066,41 @@ function updateCollapseToggleIcon() { const $collapseToggle = $('#rpg-collapse-toggle'); const $panel = $('#rpg-companion-panel'); const $icon = $collapseToggle.find('i'); - const isCollapsed = $panel.hasClass('rpg-collapsed'); + const isMobile = window.innerWidth <= 1000; - if (isCollapsed) { - // When collapsed, arrow points inward (to expand) - if ($panel.hasClass('rpg-position-right')) { - $icon.removeClass('fa-chevron-right').addClass('fa-chevron-left'); - } else if ($panel.hasClass('rpg-position-left')) { - $icon.removeClass('fa-chevron-left').addClass('fa-chevron-right'); + if (isMobile) { + // Mobile: slides from right, use same icon logic as desktop right panel + const isOpen = $panel.hasClass('rpg-mobile-open'); + console.log('[RPG Mobile] updateCollapseToggleIcon:', { + isMobile: true, + isOpen, + settingIcon: isOpen ? 'chevron-left' : 'chevron-right' + }); + if (isOpen) { + // Panel open - chevron points left (to close/slide back right) + $icon.removeClass('fa-chevron-down fa-chevron-up fa-chevron-right').addClass('fa-chevron-left'); + } else { + // Panel closed - chevron points right (to open/slide in from right) + $icon.removeClass('fa-chevron-down fa-chevron-up fa-chevron-left').addClass('fa-chevron-right'); } } else { - // When expanded, arrow points outward (to collapse) - if ($panel.hasClass('rpg-position-right')) { - $icon.removeClass('fa-chevron-left').addClass('fa-chevron-right'); - } else if ($panel.hasClass('rpg-position-left')) { - $icon.removeClass('fa-chevron-right').addClass('fa-chevron-left'); + // Desktop: icon direction based on panel position and collapsed state + const isCollapsed = $panel.hasClass('rpg-collapsed'); + + if (isCollapsed) { + // When collapsed, arrow points inward (to expand) + if ($panel.hasClass('rpg-position-right')) { + $icon.removeClass('fa-chevron-right').addClass('fa-chevron-left'); + } else if ($panel.hasClass('rpg-position-left')) { + $icon.removeClass('fa-chevron-left').addClass('fa-chevron-right'); + } + } else { + // When expanded, arrow points outward (to collapse) + if ($panel.hasClass('rpg-position-right')) { + $icon.removeClass('fa-chevron-left').addClass('fa-chevron-right'); + } else if ($panel.hasClass('rpg-position-left')) { + $icon.removeClass('fa-chevron-right').addClass('fa-chevron-left'); + } } } } @@ -1128,10 +2157,17 @@ function updateSectionVisibility() { function applyPanelPosition() { if (!$panelContainer) return; + const isMobile = window.innerWidth <= 1000; + // Remove all position classes $panelContainer.removeClass('rpg-position-left rpg-position-right rpg-position-top'); - // Add the appropriate position class + // On mobile, don't apply desktop position classes + if (isMobile) { + return; + } + + // Desktop: Add the appropriate position class $panelContainer.addClass(`rpg-position-${extensionSettings.panelPosition}`); // Update collapse toggle icon direction for new position @@ -1826,29 +2862,6 @@ function renderUserStats() { $userStatsContainer.html(html); - // Add event listeners for classic stat buttons - $('.rpg-stat-increase').on('click', function() { - const stat = $(this).data('stat'); - if (extensionSettings.classicStats[stat] < 100) { - extensionSettings.classicStats[stat]++; - saveSettings(); - saveChatData(); - // Update only the specific stat value, not the entire stats panel - $(this).closest('.rpg-classic-stat').find('.rpg-classic-stat-value').text(extensionSettings.classicStats[stat]); - } - }); - - $('.rpg-stat-decrease').on('click', function() { - const stat = $(this).data('stat'); - if (extensionSettings.classicStats[stat] > 1) { - extensionSettings.classicStats[stat]--; - saveSettings(); - saveChatData(); - // Update only the specific stat value, not the entire stats panel - $(this).closest('.rpg-classic-stat').find('.rpg-classic-stat-value').text(extensionSettings.classicStats[stat]); - } - }); - // Add event listeners for editable stat values $('.rpg-editable-stat').on('blur', function() { const field = $(this).data('field'); @@ -2860,7 +3873,32 @@ function createThoughtPanel($message, thoughtsArray) { let iconTop = avatarRect.top; let iconLeft; - if (panelPosition === 'left') { + // Detect mobile viewport (matches CSS breakpoint) + const isMobile = window.innerWidth <= 1000; + + if (isMobile) { + // On mobile: position icon horizontally centered on avatar + // The CSS transform will shift it upward by 60px + iconTop = avatarRect.top; // Start at avatar top (CSS will move it up) + iconLeft = avatarRect.left + (avatarRect.width / 2) - 18; // Centered horizontally (18px = half of 36px icon width) + + // Center the thought panel horizontally on mobile + left = window.innerWidth / 2 - panelWidth / 2; + top = avatarRect.top + avatarRect.height + 60; // Position below icon with spacing + + // No side-specific classes on mobile + $thoughtPanel.removeClass('rpg-thought-panel-left rpg-thought-panel-right'); + $thoughtIcon.removeClass('rpg-thought-icon-left rpg-thought-icon-right'); + + console.log('[RPG Companion] Mobile thought icon positioning:', { + isMobile, + windowWidth: window.innerWidth, + avatarLeft: avatarRect.left, + avatarWidth: avatarRect.width, + iconLeft, + iconTop + }); + } else if (panelPosition === 'left') { // Main panel is on left, so thought bubble goes to RIGHT side // Mirror the left side positioning: bubble should be same distance from avatar // but on the opposite side, extending to the right @@ -2928,8 +3966,9 @@ function createThoughtPanel($message, thoughtsArray) { right: 'auto' // Clear any right positioning }); - // Initially hide the icon - $thoughtIcon.hide(); + // Initially hide the panel and show the icon + $thoughtPanel.hide(); + $thoughtIcon.show(); // console.log('[RPG Companion] Thought panel created at:', { top, left }); @@ -2956,7 +3995,10 @@ function createThoughtPanel($message, thoughtsArray) { updateCharacterField(character, field, value); }); - // Update position on scroll + // RAF throttling for smooth position updates + let positionUpdateRaf = null; + + // Update position on scroll with RAF throttling const updatePanelPosition = () => { if (!$message.is(':visible')) { $thoughtPanel.hide(); @@ -2964,47 +4006,57 @@ function createThoughtPanel($message, thoughtsArray) { return; } - const newAvatarRect = $avatar[0].getBoundingClientRect(); - const newTop = newAvatarRect.top + (newAvatarRect.height / 2); - const newIconTop = newAvatarRect.top; - let newLeft, newIconLeft; - - if (panelPosition === 'left') { - // Position at chat's right edge, extending right - const chatContainer = $('#chat')[0]; - const chatRect = chatContainer ? chatContainer.getBoundingClientRect() : { right: window.innerWidth }; - newLeft = chatRect.right + panelMargin; - newIconLeft = chatRect.right + 10; - - $thoughtPanel.css({ - top: `${newTop}px`, - left: `${newLeft}px`, - right: 'auto' - }); - } else { - // Left position relative to avatar - newLeft = newAvatarRect.left - panelWidth - panelMargin; - newIconLeft = newAvatarRect.left - 40; - - $thoughtPanel.css({ - top: `${newTop}px`, - left: `${newLeft}px`, - right: 'auto' - }); + // Cancel any pending RAF + if (positionUpdateRaf) { + cancelAnimationFrame(positionUpdateRaf); } - $thoughtIcon.css({ - top: `${newIconTop}px`, - left: `${newIconLeft}px`, - right: 'auto' + // Schedule update on next frame + positionUpdateRaf = requestAnimationFrame(() => { + const newAvatarRect = $avatar[0].getBoundingClientRect(); + const newTop = newAvatarRect.top + (newAvatarRect.height / 2); + const newIconTop = newAvatarRect.top; + let newLeft, newIconLeft; + + if (panelPosition === 'left') { + // Position at chat's right edge, extending right + const chatContainer = $('#chat')[0]; + const chatRect = chatContainer ? chatContainer.getBoundingClientRect() : { right: window.innerWidth }; + newLeft = chatRect.right + panelMargin; + newIconLeft = chatRect.right + 10; + + $thoughtPanel.css({ + top: `${newTop}px`, + left: `${newLeft}px`, + right: 'auto' + }); + } else { + // Left position relative to avatar + newLeft = newAvatarRect.left - panelWidth - panelMargin; + newIconLeft = newAvatarRect.left - 40; + + $thoughtPanel.css({ + top: `${newTop}px`, + left: `${newLeft}px`, + right: 'auto' + }); + } + + $thoughtIcon.css({ + top: `${newIconTop}px`, + left: `${newIconLeft}px`, + right: 'auto' + }); + + if ($thoughtPanel.is(':visible')) { + $thoughtPanel.show(); + } + if ($thoughtIcon.is(':visible')) { + $thoughtIcon.show(); + } + + positionUpdateRaf = null; }); - - if ($thoughtPanel.is(':visible')) { - $thoughtPanel.show(); - } - if ($thoughtIcon.is(':visible')) { - $thoughtIcon.show(); - } }; // Update position on scroll and resize diff --git a/style.css b/style.css index 8313086..eaeddfa 100644 --- a/style.css +++ b/style.css @@ -2175,47 +2175,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; @@ -2223,216 +2475,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; @@ -2457,6 +2513,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 ============================================ */ @@ -2558,94 +2643,135 @@ body:has(.rpg-panel.rpg-position-left) #sheld { } /* ============================================ - SETTINGS POPUP + SETTINGS MODAL - MOBILE FIRST ============================================ */ + +/* CSS Custom Properties for Responsive Scaling */ +.rpg-settings-popup { + /* Fluid spacing */ + --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-heading: clamp(0.9rem, 3.5vw, 1rem); + + /* Content constraints - more height than dice roller */ + --modal-max-width: min(90vw, 500px); + --modal-max-height: 75vh; +} + +/* Modal Container - Hidden by default */ .rpg-settings-popup { position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; + inset: 0; z-index: 10000; - display: flex; + display: none; align-items: center; justify-content: center; + padding: 0.5rem; } -.rpg-settings-popup-overlay { +/* Open state */ +.rpg-settings-popup.is-open { + display: flex; + animation: fadeIn 0.2s ease-out; +} + +/* Closing state */ +.rpg-settings-popup.is-closing { + display: flex; + animation: fadeOut 0.2s ease-in; +} + +/* Backdrop overlay - using ::before pseudo-element */ +.rpg-settings-popup::before { + content: ''; position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.7); + inset: 0; + background: rgba(0, 0, 0, 0.85); backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); } +/* Modal Content Box */ .rpg-settings-popup-content { position: relative; - background: var(--rpg-bg); - border: 3px solid var(--rpg-border); - border-radius: 0.938em; - max-width: 31.25rem; - width: 90%; - max-height: 80vh; - overflow: hidden; + 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; - box-shadow: 0 10px 50px rgba(0, 0, 0, 0.9); - animation: popupSlideIn 0.3s ease-out; - color: var(--rpg-text); + overflow: hidden; + animation: slideInUp 0.3s cubic-bezier(0.16, 1, 0.3, 1); + margin: auto 0; } +/* Header */ .rpg-settings-popup-header { display: flex; align-items: center; justify-content: space-between; - padding: 0.938em 1.25em; + padding: var(--modal-padding); background: var(--rpg-accent); - border-bottom: 1px solid var(--rpg-border); + border-bottom: var(--modal-border-width) solid var(--rpg-border); + flex-shrink: 0; } .rpg-settings-popup-header h3 { margin: 0; - font-size: 1.125rem; + font-size: var(--modal-font-heading); color: var(--rpg-highlight); display: flex; align-items: center; - gap: 0.625em; + gap: var(--modal-gap); } +/* Close button - touch-friendly */ .rpg-popup-close { background: transparent; border: none; color: var(--rpg-text); font-size: 1.5rem; cursor: pointer; - padding: 0; - width: 1.875rem; - height: 1.875rem; + padding: 0.5rem; + min-width: 44px; + min-height: 44px; display: flex; align-items: center; justify-content: center; - border-radius: 0.25em; + border-radius: 0.25rem; transition: all 0.2s ease; } -.rpg-popup-close:hover { +.rpg-popup-close:active { background: rgba(255, 255, 255, 0.1); color: var(--rpg-highlight); } +/* Scrollable Body */ .rpg-settings-popup-body { - padding: 1.25em; + padding: var(--modal-padding); overflow-y: auto; - flex: 1; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; + flex: 1 1 auto; + min-height: 0; } +/* Settings Groups */ .rpg-settings-group { - margin-bottom: 1.25em; - padding-bottom: 1.25em; + margin-bottom: var(--modal-padding); + padding-bottom: var(--modal-padding); border-bottom: 1px solid var(--rpg-border); } @@ -2654,15 +2780,16 @@ body:has(.rpg-panel.rpg-position-left) #sheld { } .rpg-settings-group h4 { - margin: 0 0 0.938em 0; - font-size: 0.938rem; + margin: 0 0 var(--modal-gap) 0; + font-size: var(--modal-font-base); color: var(--rpg-highlight); display: flex; align-items: center; - gap: 0.5em; + gap: var(--modal-gap); + font-weight: 600; } -/* Apply theme to settings popup */ +/* Theme Support */ #rpg-settings-popup[data-theme="sci-fi"] .rpg-settings-popup-content { --rpg-bg: #0a0e27; --rpg-accent: #1a1f3a; @@ -2687,6 +2814,24 @@ body:has(.rpg-panel.rpg-position-left) #sheld { --rpg-border: #ff00ff; } +/* Desktop Enhancement (1001px+) */ +@media (min-width: 1001px) { + .rpg-settings-popup { + --modal-padding: 1rem; + --modal-gap: 0.75rem; + --modal-font-base: 0.9rem; + --modal-font-small: 0.8rem; + --modal-font-heading: 1.125rem; + --modal-max-width: 600px; + } + + /* Hover effects on desktop */ + .rpg-popup-close:hover { + background: rgba(255, 255, 255, 0.1); + color: var(--rpg-highlight); + } +} + /* ============================================ CHAT THOUGHT OVERLAYS ============================================ */ @@ -2991,20 +3136,30 @@ body:has(.rpg-panel.rpg-position-left) #sheld { /* Hide mobile toggle on desktop, show on mobile */ .rpg-mobile-toggle { display: none; + align-items: center; + justify-content: center; position: fixed; - bottom: 5rem; - right: 1.25rem; - width: 3.5rem; - height: 3.5rem; + /* Position set by JavaScript based on saved settings */ + width: 44px; + height: 44px; border-radius: 50%; - background: var(--rpg-accent, #2c3e50); - border: 3px solid var(--rpg-border, #34495e); + background: var(--SmartThemeBlurTintColor); + border: 2px solid var(--SmartThemeBorderColor); color: var(--rpg-text, #ecf0f1); - font-size: 1.5rem; - cursor: pointer; - z-index: 999; + font-size: 1.25rem; + cursor: grab; + z-index: 10002; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - transition: all 0.3s ease; + transition: opacity 0.3s ease, transform 0.2s ease, top 0.3s ease, left 0.3s ease, right 0.3s ease, bottom 0.3s ease; + user-select: none; /* Prevent text selection while dragging */ + -webkit-user-select: none; + will-change: top, left; /* Optimize for position changes */ +} + +/* Disable transitions while actively dragging */ +.rpg-mobile-toggle.dragging { + transition: none; + cursor: grabbing; } .rpg-mobile-toggle:hover { @@ -3034,13 +3189,32 @@ body:has(.rpg-panel.rpg-position-left) #sheld { backdrop-filter: blur(2px); } -/* Mobile-specific panel behavior */ -@media (max-width: 768px) { - /* Show the FAB on mobile */ +/* Mobile-specific panel behavior - matches SillyTavern's 1000px breakpoint */ +/* CACHE BUST v2025-01-16 */ +@media (max-width: 1000px) { + /* ======================================== + MOBILE PANEL FOUNDATION + ======================================== */ + + /* Show the mobile FAB toggle button */ .rpg-mobile-toggle { display: flex; - align-items: center; - justify-content: center; + } + + /* Hide FAB when panel is open */ + body:has(.rpg-panel.rpg-mobile-open) .rpg-mobile-toggle { + opacity: 0; + pointer-events: none; + } + + /* Hide internal collapse toggle when panel is closed on mobile */ + .rpg-collapse-toggle { + display: none !important; + } + + /* Show internal collapse toggle when panel is open on mobile */ + .rpg-panel.rpg-mobile-open .rpg-collapse-toggle { + display: flex !important; } /* Show overlay when needed */ @@ -3048,39 +3222,502 @@ body:has(.rpg-panel.rpg-position-left) #sheld { display: block; } - /* Hide panel by default on mobile */ + /* Remove margin adjustments on mobile - content takes full width */ + body:has(.rpg-panel) #sheld { + margin-right: 0 !important; + margin-left: 0 !important; + } + + /* Mobile panel - slide from right like desktop */ .rpg-panel { - transform: translateY(100%); - transition: transform 0.3s ease-in-out; - } + position: fixed !important; + top: var(--topBarBlockSize) !important; + right: 0 !important; + bottom: 0 !important; + left: auto !important; - /* Show panel when opened */ - .rpg-panel.rpg-mobile-open { - transform: translateY(0); - z-index: 999; - } + /* Mobile panel sizing */ + width: 85dvw !important; + max-width: 400px !important; + height: calc(100dvh - var(--topBarBlockSize)) !important; - /* On mobile, panel is always at bottom */ - .rpg-panel.rpg-position-right, - .rpg-panel.rpg-position-left, - .rpg-panel.rpg-position-top { - position: fixed; - top: var(--topBarBlockSize); - bottom: 0; - left: 0; - right: 0; - width: 100%; - max-width: 100%; - border-radius: 1.25em 1.25em 0 0; - overflow-y: scroll; + /* Hidden by default - completely removed from layout */ + display: none !important; + + overflow-y: auto !important; -webkit-overflow-scrolling: touch; + + /* Styling */ + border-radius: 20px 0 0 0; + border-left: 1px solid var(--SmartThemeBorderColor); + border-top: 1px solid var(--SmartThemeBorderColor); + backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2)); + box-shadow: -5px 0 20px var(--rpg-shadow); + } + + /* Show panel when opened with slide-in animation */ + .rpg-panel.rpg-mobile-open { + display: block !important; + z-index: 50; + animation: rpgSlideInFromRight 0.3s ease-in-out; + } + + /* Closing animation - slide out to right */ + .rpg-panel.rpg-mobile-closing { + display: block !important; + z-index: 50; + animation: rpgSlideOutToRight 0.3s ease-in-out; + } + + /* Slide-in animation from right */ + @keyframes rpgSlideInFromRight { + from { + transform: translateX(100%); + } + to { + transform: translateX(0); + } + } + + /* Slide-out animation to right */ + @keyframes rpgSlideOutToRight { + from { + transform: translateX(0); + } + to { + transform: translateX(100%); + } + } + + /* ======================================== + MOBILE KEYBOARD HANDLING + ======================================== */ + + /* When mobile keyboard is visible, adjust panel layout to prevent squashing */ + .rpg-panel.rpg-keyboard-visible { + /* Prevent content from being pushed too far up */ + padding-bottom: 20px; + } + + /* Make sections more compact when keyboard visible */ + .rpg-panel.rpg-keyboard-visible .rpg-stats-section, + .rpg-panel.rpg-keyboard-visible .rpg-info-section, + .rpg-panel.rpg-keyboard-visible .rpg-thoughts-section { + padding: 8px 12px; + } + + /* Reduce spacing in stat bars when keyboard visible */ + .rpg-panel.rpg-keyboard-visible .rpg-stats-grid { + gap: 4px; + } + + /* Disable collapsed state on mobile */ + .rpg-panel.rpg-collapsed { + max-width: 100dvw !important; + min-width: unset !important; + width: 100dvw !important; + } + + .rpg-panel.rpg-collapsed .rpg-game-container { + opacity: 1 !important; + pointer-events: auto !important; + } + + /* Collapse toggle on mobile - right side, always visible */ + .rpg-collapse-toggle { + display: flex !important; + align-items: center; + justify-content: center; + position: fixed !important; + top: calc(var(--topBarBlockSize) + 120px) !important; + left: 12px !important; + right: auto !important; + width: 44px; + height: 44px; + min-width: 44px; + min-height: 44px; + border-radius: 50%; + background: var(--SmartThemeBlurTintColor); + border: 2px solid var(--SmartThemeBorderColor); + z-index: 9999 !important; + transition: all 0.2s ease; + transform: none !important; + pointer-events: auto !important; + cursor: pointer !important; + } + + .rpg-collapse-toggle:hover { + background: rgba(255, 255, 255, 0.1); + transform: scale(1.05) !important; + } + + .rpg-collapse-toggle:active { + transform: scale(0.95) !important; + } + + /* Mobile icon styling - use chevrons for drawer UX */ + .rpg-collapse-toggle i { + transform: none !important; + font-size: 20px; + } + + /* ======================================== + MOBILE TAB NAVIGATION + ======================================== */ + + /* Mobile tab container wrapper */ + .rpg-mobile-container { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + margin: -12px -12px 16px -12px; + } + + /* Tab container at top of panel */ + .rpg-mobile-tabs { + display: flex; + position: sticky; + top: 0; + z-index: 10; + background: var(--SmartThemeBlurTintColor); + backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 1.5)); + border-bottom: 2px solid var(--SmartThemeBorderColor); + margin: 0; + padding: 0; + } + + .rpg-mobile-tab { + flex: 1; + height: 44px; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + font-size: 14px; + font-weight: 500; + color: var(--SmartThemeBodyColor); + background: transparent; + border: none; + border-bottom: 3px solid transparent; + cursor: pointer; + transition: all 0.2s ease; + padding: 0 8px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .rpg-mobile-tab:active { + transform: scale(0.97); + } + + .rpg-mobile-tab.active { + color: var(--SmartThemeQuoteColor); + border-bottom-color: var(--SmartThemeQuoteColor); + background: rgba(255, 255, 255, 0.05); + } + + .rpg-mobile-tab i { + font-size: 16px; + } + + /* Tab content sections */ + .rpg-mobile-tab-content { + display: none; + animation: fadeIn 0.2s ease; + } + + .rpg-mobile-tab-content.active { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + padding: 12px; + } + + @keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } + } + + /* Combined Info & Characters wrapper */ + .rpg-mobile-combined-content { + display: flex; + flex-direction: column; + gap: 0; + height: 100%; + min-height: 0; + } + + /* Info Box takes fixed 50% of vertical space */ + .rpg-mobile-combined-content > #rpg-info-box { + flex: 0 0 50%; + min-height: 0; + overflow-y: auto; + padding-bottom: 16px; + display: flex; + flex-direction: column; + gap: 0.5em; + } + + /* Characters section takes remaining 50% */ + .rpg-mobile-combined-content > #rpg-thoughts { + flex: 1; + min-height: 0; + overflow-y: auto; + padding-bottom: 16px; + } + + /* Add divider between Info and Characters */ + .rpg-mobile-combined-content > .rpg-section:not(:last-child) { + border-bottom: 1px solid var(--SmartThemeBorderColor); + margin-bottom: 16px; + } + + /* Hide dividers on mobile (tabs handle separation) */ + .rpg-divider { + display: none; + } + + /* ======================================== + MOBILE INFO BOX IMPROVEMENTS + ======================================== */ + + /* Rows scale proportionally to fill Info Box */ + .rpg-dashboard-row-1 { + flex: 1.2 !important; /* Slightly more space for 4 widgets */ + display: flex !important; + gap: 0.25em; + } + + .rpg-dashboard-row-2 { + flex: 0.8 !important; /* Less space for 1 widget */ + display: flex !important; + } + + /* Widgets fill their row height */ + .rpg-dashboard-row-1 .rpg-dashboard-widget, + .rpg-dashboard-row-2 .rpg-dashboard-widget { + height: 100% !important; + flex: 1 !important; + display: flex !important; + flex-direction: column !important; + justify-content: center; + } + + /* ======================================== + MOBILE STATS TAB LAYOUT IMPROVEMENTS + ======================================== */ + + /* Make the entire stats section a grid */ + .rpg-stats-section { + display: grid !important; + grid-template-columns: 40% 60%; /* Left for inventory/mood, right for attributes */ + grid-template-rows: auto auto auto auto; /* Portrait, stat bars, inventory, mood */ + gap: 12px; + padding: 16px 12px; + } + + /* Use display: contents so children participate in grid */ + .rpg-stats-header, + .rpg-stats-content { + display: contents !important; + } + + /* Portrait row - centered at top, full width */ + .rpg-stats-header-left { + grid-column: 1 / 3; + grid-row: 1; + display: flex !important; + justify-content: center; + align-items: center; + gap: 4px; + } + + .rpg-user-portrait { + width: 64px; + height: 64px; + } + + /* Hide stats title on mobile */ + .rpg-stats-title { + display: none; + } + + /* Stat bars row - full width */ + .rpg-stats-left { + grid-column: 1 / 3; + grid-row: 2; + display: contents !important; + } + + .rpg-stats-grid { + grid-column: 1 / 3; + grid-row: 2; + } + + /* Inventory - bottom left */ + .rpg-inventory-box { + grid-column: 1; + grid-row: 3; + margin: 0; + min-height: auto; + max-height: none; + max-width: 100%; /* Override 12.5rem restriction */ + } + + /* Mood - below inventory on left */ + .rpg-mood { + grid-column: 1; + grid-row: 4; + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; + } + + /* Attributes - right side, spanning rows 3-4 */ + .rpg-stats-right { + grid-column: 2; + grid-row: 3 / 5; + display: contents !important; + } + + .rpg-classic-stats { + grid-column: 2; + grid-row: 3 / 5; + } + + /* Attributes as ultra-compact 2x3 grid for mobile */ + .rpg-classic-stats-grid { + display: grid !important; + grid-template-columns: repeat(2, 1fr) !important; + grid-template-rows: repeat(3, 1fr) !important; + gap: 3px; + margin: 4px 5px 4px 0; + padding-right: 5px; + } + + /* Each attribute - ULTRA COMPACT with vertical stack layout */ + .rpg-classic-stat { + display: grid !important; + grid-template-columns: 1fr 1fr; + grid-template-rows: auto auto; + gap: 2px 4px; + padding: 4px 6px; + min-height: auto; + height: auto; + background: var(--SmartThemeBlurTintColor); + border: 1px solid var(--SmartThemeBorderColor); + border-radius: 4px; + } + + /* Make buttons container children participate in parent grid */ + .rpg-classic-stat-buttons { + display: contents; + } + + /* Label - top left */ + .rpg-classic-stat-label { + grid-column: 1; + grid-row: 1; + font-size: 9px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.3px; + align-self: center; + justify-self: center; + } + + /* Value - top right */ + .rpg-classic-stat-value { + grid-column: 2; + grid-row: 1; + font-size: 14px; + font-weight: 700; + color: var(--SmartThemeQuoteColor); + text-align: center; + align-self: center; + justify-self: center; + } + + /* Minus button - bottom left */ + .rpg-stat-decrease { + grid-column: 1; + grid-row: 2; + justify-self: center; + } + + /* Plus button - bottom right */ + .rpg-stat-increase { + grid-column: 2; + grid-row: 2; + justify-self: center; + } + + .rpg-classic-stat-btn { + min-width: 24px !important; + min-height: 24px !important; + width: 24px; + height: 24px; + font-size: 14px; + font-weight: 700; + border-radius: 4px; + display: flex !important; + align-items: center; + justify-content: center; + padding: 0; + cursor: pointer; + user-select: none; + background: var(--rpg-accent); + border: 1px solid var(--rpg-border); + color: var(--rpg-text); + } + + .rpg-classic-stat-btn:hover { + background: var(--rpg-highlight); + border-color: var(--rpg-highlight); + transform: scale(1.05); + } + + .rpg-classic-stat-btn:active { + transform: scale(0.95); + } + + /* ======================================== + MOBILE THOUGHT ICON POSITIONING + ======================================== */ + + /* Position thought icon above avatar on mobile to prevent off-screen clipping */ + /* JavaScript will calculate position, but add transform to move it above and right */ + #rpg-thought-icon { + /* Use transform to shift icon above and to the right of avatar */ + transform: translate(50px, -45px) !important; + /* Smooth animation for position changes during scroll */ + transition: top 0.2s ease-out, left 0.2s ease-out !important; + will-change: top, left; + } + + /* ======================================== + MOBILE CHARACTER RELATIONSHIP BADGE + ======================================== */ + + /* Keep relationship badge small on mobile to prevent it from covering avatar */ + .rpg-relationship-badge { + width: 18px !important; + height: 18px !important; + font-size: 10px !important; + padding: 0 !important; + min-height: unset !important; + line-height: 18px !important; } } /* Extra small screens - adjust FAB position */ @media (max-width: 480px) { .rpg-mobile-toggle { - bottom: 4.375rem; + bottom: 6rem; right: 0.938rem; width: 3.25rem; height: 3.25rem; @@ -3090,13 +3727,6 @@ body:has(.rpg-panel.rpg-position-left) #sheld { /* Touch-friendly improvements for mobile */ @media (max-width: 768px) { - /* Larger touch targets for buttons */ - .rpg-classic-stat-btn { - min-width: 2.75rem; - min-height: 2.75rem; - font-size: 1.125rem; - } - /* More padding for editable fields */ .rpg-editable { padding: 0.5em; diff --git a/template.html b/template.html index 3815c90..6d30c04 100644 --- a/template.html +++ b/template.html @@ -68,17 +68,19 @@ - -