From 9ed76a43811a64b6572d5c4d39fd11dba089a7e2 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Thu, 16 Oct 2025 01:35:15 +1100 Subject: [PATCH 01/16] feat: implement comprehensive mobile UX for RPG Companion panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add bottom-sliding drawer system for mobile (≤1000px viewport) - Implement tabbed navigation with Stats and Info & Characters tabs - Combine Info Box and Present Characters into single tab with 50/50 split - Add smooth transitions between desktop and mobile layouts - Reposition collapse button as close button on mobile - Implement FAB toggle button for opening mobile drawer Mobile Stats Tab: - Use CSS Grid layout for efficient space utilization - Portrait centered at top, stat bars below - Inventory and mood on left, attributes list on right - Convert attributes from 3x2 grid to vertical list Mobile Info Box: - Scale dashboard widgets to fill allocated space - Proportional row heights (60% top row, 40% location) - Widgets expand to fill available vertical space Technical improvements: - Bottom-based drawer positioning instead of transform - CSS-only transitions, JavaScript only toggles classes - Instant tab setup on desktop→mobile for smooth transition - Temporary transition disabling for mobile→desktop snap - Proper flex hierarchy for space filling --- index.js | 216 ++++++++++++++++++++++++++++ style.css | 412 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 602 insertions(+), 26 deletions(-) diff --git a/index.js b/index.js index 488a744..95458ed 100644 --- a/index.js +++ b/index.js @@ -1010,6 +1010,211 @@ function setupMobileToggle() { }); } }); + + // 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) { + // 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 - CSS handles smooth transition + $panel.removeClass('rpg-mobile-open'); + $mobileToggle.removeClass('active'); + $('.rpg-mobile-overlay').remove(); + + // Set up mobile tabs IMMEDIATELY (no debounce delay) + setupMobileTabs(); + + 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'); + $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; + }, 150); // Debounce only for mobile→desktop + }); + + // Initialize mobile tabs if starting on mobile + const isMobile = window.innerWidth <= 1000; + if (isMobile) { + setupMobileTabs(); + } +} + +/** + * 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(); } /** @@ -1021,6 +1226,17 @@ function setupCollapseToggle() { const $icon = $collapseToggle.find('i'); $collapseToggle.on('click', function() { + const isMobile = window.innerWidth <= 1000; + + // On mobile: button acts as close button for mobile panel + if (isMobile) { + $panel.removeClass('rpg-mobile-open'); + $('.rpg-mobile-overlay').remove(); + $('#rpg-mobile-toggle').removeClass('active'); + return; + } + + // Desktop behavior: collapse/expand side panel const isCollapsed = $panel.hasClass('rpg-collapsed'); if (isCollapsed) { diff --git a/style.css b/style.css index 08e70a3..6029671 100644 --- a/style.css +++ b/style.css @@ -2974,8 +2974,12 @@ body:has(.rpg-panel.rpg-position-left) #sheld { backdrop-filter: blur(2px); } -/* Mobile-specific panel behavior */ -@media (max-width: 768px) { +/* Mobile-specific panel behavior - matches SillyTavern's 1000px breakpoint */ +@media (max-width: 1000px) { + /* ======================================== + MOBILE PANEL FOUNDATION + ======================================== */ + /* Show the FAB on mobile */ .rpg-mobile-toggle { display: flex; @@ -2988,32 +2992,388 @@ 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; + } + + /* CLEAN SLATE: Reset ALL desktop positioning */ .rpg-panel { - transform: translateY(100%); - transition: transform 0.3s ease-in-out; - } - - /* Show panel when opened */ - .rpg-panel.rpg-mobile-open { - transform: translateY(0); - z-index: 999; - } - - /* 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; + position: fixed !important; + left: 0 !important; + right: 0 !important; + top: auto !important; + width: 100dvw !important; + max-width: 100dvw !important; + min-width: unset !important; + height: calc(100dvh - var(--topBarBlockSize)) !important; + max-height: calc(100dvh - var(--topBarBlockSize)) !important; + border-radius: 20px 20px 0 0; + overflow-y: auto; -webkit-overflow-scrolling: touch; + box-shadow: 0 -5px 20px var(--rpg-shadow); + border-left: 1px solid var(--SmartThemeBorderColor); + border-right: 1px solid var(--SmartThemeBorderColor); + border-top: 1px solid var(--SmartThemeBorderColor); + backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2)); + + /* DRAWER POSITIONING: Starts below viewport */ + bottom: calc(-100dvh + var(--topBarBlockSize)); + + /* ONLY transition bottom property */ + transition: bottom 0.3s ease-in-out; + } + + /* Show panel when opened - slides up */ + .rpg-panel.rpg-mobile-open { + bottom: 0; + z-index: 50; + } + + /* 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; + } + + /* Reposition collapse toggle on mobile as close button inside panel */ + .rpg-collapse-toggle { + display: flex !important; + align-items: center; + justify-content: center; + position: absolute; + top: 12px; + right: 12px; + left: 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: 100; + transition: all 0.2s ease; + transform: none !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; + } + + /* Change icon to X on mobile */ + .rpg-collapse-toggle i { + transform: none !important; + } + + .rpg-collapse-toggle i::before { + content: "\f00d" !important; /* fa-times (X icon) */ + 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 vertical list instead of grid */ + .rpg-classic-stats-grid { + display: flex !important; + flex-direction: column !important; + gap: 10px; + margin: 12px 0; + grid-template-columns: none !important; + grid-template-rows: none !important; + } + + /* Each attribute as horizontal row */ + .rpg-classic-stat { + display: flex !important; + flex-direction: row !important; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + gap: 12px; + min-height: auto; + height: auto; + background: var(--SmartThemeBlurTintColor); + border: 1px solid var(--SmartThemeBorderColor); + border-radius: 10px; + } + + /* Label on left */ + .rpg-classic-stat-label { + font-size: 14px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + min-width: 50px; + flex-shrink: 0; + } + + /* Value in middle */ + .rpg-classic-stat-value { + font-size: 24px; + font-weight: 700; + color: var(--SmartThemeQuoteColor); + flex: 1; + text-align: center; + } + + /* Buttons on right */ + .rpg-classic-stat-controls { + display: flex; + gap: 8px; + flex-shrink: 0; + } + + .rpg-classic-stat-btn { + min-width: 40px !important; + min-height: 40px !important; + width: 40px; + height: 40px; + font-size: 16px; + font-weight: 700; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + padding: 0; } } From 9a653a9c7a9a7a653f0dbb8f0b801a8ebc4dff3b Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Thu, 16 Oct 2025 09:43:23 +1100 Subject: [PATCH 02/16] feat: implement draggable mobile FAB with comprehensive event handling Add full mouse and touch support for mobile toggle button with drag-to-reposition functionality and persistent position saving. Changes: - Add mobile FAB button with draggable positioning (both mouse and touch) - Implement time-based drag detection (200ms or 10px threshold) - Add position persistence in extension settings - Fix click handler to work on both mobile and desktop viewports - Add comprehensive diagnostic logging for event debugging - Update mobile panel to slide from right (matching desktop UX) - Implement dual-button pattern (FAB + internal collapse toggle) - Add viewport-constrained dragging with 10px padding - Prevent click events from firing after drag completion Mobile UX: - FAB visible when panel closed, hidden when open - Internal collapse toggle visible only when panel open - Touch and mouse drag support with real-time positioning - Click/tap toggles panel, drag repositions button - Position saved across sessions Technical: - Add mousedown/mousemove/mouseup handlers for desktop drag - Add touchstart/touchmove/touchend handlers for mobile drag - Remove broken viewport check that prevented mobile clicks - Add 'just-dragged' flag to prevent click after drag - Update .gitignore to exclude CLAUDE.md and .env files --- .gitignore | 6 + index.js | 478 ++++++++++++++++++++++++++++++++++++++++++++++++++--- style.css | 101 ++++++----- 3 files changed, 520 insertions(+), 65 deletions(-) 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 95458ed..90258b6 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, @@ -989,21 +993,323 @@ function setupMobileToggle() { const $panel = $('#rpg-companion-panel'); const $overlay = $('
'); - // Toggle panel visibility on mobile - $mobileToggle.on('click', function() { + // 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); + } + + // 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 + + // Touch start - begin tracking + $mobileToggle.on('touchstart', function(e) { + console.log('[RPG Mobile] >>> TOUCHSTART EVENT FIRED <<<'); + + 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; + + console.log('[RPG Mobile] Touch start:', { + time: touchStartTime, + touchX: touchStartX, + touchY: touchStartY, + buttonX: buttonStartX, + buttonY: buttonStartY + }); + }); + + // 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); + + console.log('[RPG Mobile] >>> TOUCHMOVE EVENT FIRED <<<', { + distance: distance.toFixed(2), + timeSinceStart, + isDragging + }); + + // Start dragging if held long enough OR moved far enough + if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) { + isDragging = true; + $mobileToggle.css('transition', 'none'); // Disable transitions while dragging + console.log('[RPG Mobile] Started dragging:', { timeSinceStart, distance }); + } + + 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)); + + // Apply position + $mobileToggle.css({ + left: newX + 'px', + top: newY + 'px', + right: 'auto', + bottom: 'auto' + }); + } + }); + + // Mouse drag support for desktop + let mouseDown = false; + + $mobileToggle.on('mousedown', function(e) { + console.log('[RPG Mobile] >>> MOUSEDOWN EVENT FIRED <<<'); + + // 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; + + console.log('[RPG Mobile] Mouse down:', { + time: touchStartTime, + mouseX: touchStartX, + mouseY: touchStartY, + buttonX: buttonStartX, + buttonY: buttonStartY + }); + }); + + // 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); + + console.log('[RPG Mobile] >>> MOUSEMOVE EVENT FIRED <<<', { + distance: distance.toFixed(2), + timeSinceStart, + isDragging, + mouseDown + }); + + // Start dragging if held long enough OR moved far enough + if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) { + isDragging = true; + $mobileToggle.css('transition', 'none'); + console.log('[RPG Mobile] Started mouse dragging:', { timeSinceStart, distance }); + } + + 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)); + + // Apply position + $mobileToggle.css({ + left: newX + 'px', + top: newY + 'px', + right: 'auto', + bottom: 'auto' + }); + } + }); + + // Mouse up - save position or let click handler toggle + $(document).on('mouseup', function(e) { + if (!mouseDown) return; + + console.log('[RPG Mobile] >>> MOUSEUP EVENT FIRED <<<', { isDragging }); + + 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); + + // Re-enable transitions + setTimeout(() => { + $mobileToggle.css('transition', ''); + }, 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) { + console.log('[RPG Mobile] >>> TOUCHEND EVENT FIRED <<<', { isDragging }); + + // TEMPORARILY COMMENTED FOR DIAGNOSIS - might be blocking click fallback + // e.preventDefault(); + + console.log('[RPG Mobile] Touch end details:', { isDragging }); + + 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); + + // Re-enable transitions + setTimeout(() => { + $mobileToggle.css('transition', ''); + }, 50); + + isDragging = false; + } else { + // Was a tap - toggle panel + console.log('[RPG Mobile] Quick tap detected - toggling panel'); + + if ($panel.hasClass('rpg-mobile-open')) { + // Close panel + $panel.removeClass('rpg-mobile-open'); + $overlay.remove(); + $mobileToggle.removeClass('active'); + } else { + // Open 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'); + }); + } + } + }); + + // 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')) { - // Close panel + console.log('[RPG Mobile] Click: Closing panel'); $panel.removeClass('rpg-mobile-open'); $overlay.remove(); $mobileToggle.removeClass('active'); } else { - // Open panel + 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() { + console.log('[RPG Mobile] Overlay clicked - closing panel'); $panel.removeClass('rpg-mobile-open'); $overlay.remove(); $mobileToggle.removeClass('active'); @@ -1024,6 +1330,8 @@ function setupMobileToggle() { // 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'); @@ -1035,9 +1343,26 @@ function setupMobileToggle() { $mobileToggle.removeClass('active'); $('.rpg-mobile-overlay').remove(); + // 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; } @@ -1078,7 +1403,23 @@ function setupMobileToggle() { // 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(); } } @@ -1225,14 +1566,78 @@ 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 acts as close button for mobile panel + // On mobile: button toggles panel open/closed (same as desktop behavior) if (isMobile) { - $panel.removeClass('rpg-mobile-open'); - $('.rpg-mobile-overlay').remove(); - $('#rpg-mobile-toggle').removeClass('active'); + 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 + console.log('[RPG Mobile] Closing panel'); + $panel.removeClass('rpg-mobile-open'); + $('.rpg-mobile-overlay').remove(); + $('#rpg-mobile-toggle').removeClass('active'); + } 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'); + $panel.removeClass('rpg-mobile-open'); + $overlay.remove(); + 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; } @@ -1273,21 +1678,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-right' : 'chevron-left' + }); + if (isOpen) { + // Panel open - chevron points right (to close/slide right) + $icon.removeClass('fa-chevron-down fa-chevron-up fa-chevron-left').addClass('fa-chevron-right'); + } else { + // Panel closed - chevron points left (to open/slide left) + $icon.removeClass('fa-chevron-down fa-chevron-up fa-chevron-right').addClass('fa-chevron-left'); } } 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'); + } } } } @@ -1344,10 +1769,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 diff --git a/style.css b/style.css index 6029671..576966d 100644 --- a/style.css +++ b/style.css @@ -2931,20 +2931,23 @@ 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; + font-size: 1.25rem; cursor: pointer; - z-index: 999; + 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; + user-select: none; /* Prevent text selection while dragging */ + -webkit-user-select: none; } .rpg-mobile-toggle:hover { @@ -2975,16 +2978,31 @@ body:has(.rpg-panel.rpg-position-left) #sheld { } /* Mobile-specific panel behavior - matches SillyTavern's 1000px breakpoint */ +/* CACHE BUST v2025-01-16 */ @media (max-width: 1000px) { /* ======================================== MOBILE PANEL FOUNDATION ======================================== */ - /* Show the FAB on mobile */ + /* 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 */ @@ -2998,36 +3016,37 @@ body:has(.rpg-panel.rpg-position-left) #sheld { margin-left: 0 !important; } - /* CLEAN SLATE: Reset ALL desktop positioning */ + /* Mobile panel - slide from right like desktop */ .rpg-panel { position: fixed !important; - left: 0 !important; + top: var(--topBarBlockSize) !important; right: 0 !important; - top: auto !important; - width: 100dvw !important; - max-width: 100dvw !important; - min-width: unset !important; + bottom: 0 !important; + left: auto !important; + + /* Mobile panel sizing */ + width: 85dvw !important; + max-width: 400px !important; height: calc(100dvh - var(--topBarBlockSize)) !important; - max-height: calc(100dvh - var(--topBarBlockSize)) !important; - border-radius: 20px 20px 0 0; - overflow-y: auto; + + /* Start off-screen to the right */ + transform: translateX(100%) !important; + transition: transform 0.3s ease-in-out !important; + + overflow-y: auto !important; -webkit-overflow-scrolling: touch; - box-shadow: 0 -5px 20px var(--rpg-shadow); + + /* Styling */ + border-radius: 20px 0 0 0; border-left: 1px solid var(--SmartThemeBorderColor); - border-right: 1px solid var(--SmartThemeBorderColor); border-top: 1px solid var(--SmartThemeBorderColor); backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2)); - - /* DRAWER POSITIONING: Starts below viewport */ - bottom: calc(-100dvh + var(--topBarBlockSize)); - - /* ONLY transition bottom property */ - transition: bottom 0.3s ease-in-out; + box-shadow: -5px 0 20px var(--rpg-shadow); } - /* Show panel when opened - slides up */ + /* Show panel when opened - slides in from right */ .rpg-panel.rpg-mobile-open { - bottom: 0; + transform: translateX(0) !important; z-index: 50; } @@ -3043,14 +3062,14 @@ body:has(.rpg-panel.rpg-position-left) #sheld { pointer-events: auto !important; } - /* Reposition collapse toggle on mobile as close button inside panel */ + /* Collapse toggle on mobile - right side, always visible */ .rpg-collapse-toggle { display: flex !important; align-items: center; justify-content: center; - position: absolute; - top: 12px; - right: 12px; + position: fixed !important; + top: calc(var(--topBarBlockSize) + 60px) !important; + right: 12px !important; left: auto !important; width: 44px; height: 44px; @@ -3059,9 +3078,11 @@ body:has(.rpg-panel.rpg-position-left) #sheld { border-radius: 50%; background: var(--SmartThemeBlurTintColor); border: 2px solid var(--SmartThemeBorderColor); - z-index: 100; + z-index: 9999 !important; transition: all 0.2s ease; transform: none !important; + pointer-events: auto !important; + cursor: pointer !important; } .rpg-collapse-toggle:hover { @@ -3073,13 +3094,9 @@ body:has(.rpg-panel.rpg-position-left) #sheld { transform: scale(0.95) !important; } - /* Change icon to X on mobile */ + /* Mobile icon styling - use chevrons for drawer UX */ .rpg-collapse-toggle i { transform: none !important; - } - - .rpg-collapse-toggle i::before { - content: "\f00d" !important; /* fa-times (X icon) */ font-size: 20px; } @@ -3380,7 +3397,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld { /* 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; From b73f6d31bc4f10d1a8feb8ff54cf3ef6c189d0c2 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Thu, 16 Oct 2025 10:27:17 +1100 Subject: [PATCH 03/16] fix: correct mobile FAB chevron direction and reposition to left side - Fix inverted chevron logic: show left arrow when panel open, right arrow when closed - Move FAB button from right to left side of screen (12px from left edge) - Adjust vertical position down by 30px for better placement - Update console logging to reflect correct icon states --- index.js | 10 +++++----- style.css | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 90258b6..5ed265c 100644 --- a/index.js +++ b/index.js @@ -1686,14 +1686,14 @@ function updateCollapseToggleIcon() { console.log('[RPG Mobile] updateCollapseToggleIcon:', { isMobile: true, isOpen, - settingIcon: isOpen ? 'chevron-right' : 'chevron-left' + settingIcon: isOpen ? 'chevron-left' : 'chevron-right' }); if (isOpen) { - // Panel open - chevron points right (to close/slide right) - $icon.removeClass('fa-chevron-down fa-chevron-up fa-chevron-left').addClass('fa-chevron-right'); - } else { - // Panel closed - chevron points left (to open/slide left) + // 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 { // Desktop: icon direction based on panel position and collapsed state diff --git a/style.css b/style.css index 576966d..639f762 100644 --- a/style.css +++ b/style.css @@ -3068,9 +3068,9 @@ body:has(.rpg-panel.rpg-position-left) #sheld { align-items: center; justify-content: center; position: fixed !important; - top: calc(var(--topBarBlockSize) + 60px) !important; - right: 12px !important; - left: auto !important; + top: calc(var(--topBarBlockSize) + 120px) !important; + left: 12px !important; + right: auto !important; width: 44px; height: 44px; min-width: 44px; From f4f0ab1484a70fe767605f313e05109293496f0b Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Thu, 16 Oct 2025 10:52:09 +1100 Subject: [PATCH 04/16] feat: add smart viewport constraint for mobile FAB button - Add constrainFabToViewport() function with top-bar awareness - Only constrains when mobileFabPosition exists (user has dragged button) - Respects SillyTavern's top bar height via CSS variable - Prevents button from being hidden behind UI elements - Applies constraint after drag operations and window resize - Remove verbose debug logging from drag/touch event handlers This implements a state-driven approach where the constraint only activates for user-positioned buttons, allowing CSS defaults to work naturally while protecting custom positions from viewport changes. --- index.js | 124 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 83 insertions(+), 41 deletions(-) diff --git a/index.js b/index.js index 5ed265c..b03fe29 100644 --- a/index.js +++ b/index.js @@ -1018,6 +1018,9 @@ function setupMobileToggle() { 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 @@ -1032,8 +1035,6 @@ function setupMobileToggle() { // Touch start - begin tracking $mobileToggle.on('touchstart', function(e) { - console.log('[RPG Mobile] >>> TOUCHSTART EVENT FIRED <<<'); - const touch = e.originalEvent.touches[0]; touchStartTime = Date.now(); @@ -1045,14 +1046,6 @@ function setupMobileToggle() { buttonStartY = offset.top; isDragging = false; - - console.log('[RPG Mobile] Touch start:', { - time: touchStartTime, - touchX: touchStartX, - touchY: touchStartY, - buttonX: buttonStartX, - buttonY: buttonStartY - }); }); // Touch move - check if should start dragging @@ -1063,17 +1056,10 @@ function setupMobileToggle() { const timeSinceStart = Date.now() - touchStartTime; const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - console.log('[RPG Mobile] >>> TOUCHMOVE EVENT FIRED <<<', { - distance: distance.toFixed(2), - timeSinceStart, - isDragging - }); - // Start dragging if held long enough OR moved far enough if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) { isDragging = true; $mobileToggle.css('transition', 'none'); // Disable transitions while dragging - console.log('[RPG Mobile] Started dragging:', { timeSinceStart, distance }); } if (isDragging) { @@ -1110,8 +1096,6 @@ function setupMobileToggle() { let mouseDown = false; $mobileToggle.on('mousedown', function(e) { - console.log('[RPG Mobile] >>> MOUSEDOWN EVENT FIRED <<<'); - // Prevent default to avoid text selection e.preventDefault(); @@ -1125,14 +1109,6 @@ function setupMobileToggle() { isDragging = false; mouseDown = true; - - console.log('[RPG Mobile] Mouse down:', { - time: touchStartTime, - mouseX: touchStartX, - mouseY: touchStartY, - buttonX: buttonStartX, - buttonY: buttonStartY - }); }); // Mouse move - only track if mouse is down @@ -1144,18 +1120,10 @@ function setupMobileToggle() { const timeSinceStart = Date.now() - touchStartTime; const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - console.log('[RPG Mobile] >>> MOUSEMOVE EVENT FIRED <<<', { - distance: distance.toFixed(2), - timeSinceStart, - isDragging, - mouseDown - }); - // Start dragging if held long enough OR moved far enough if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) { isDragging = true; $mobileToggle.css('transition', 'none'); - console.log('[RPG Mobile] Started mouse dragging:', { timeSinceStart, distance }); } if (isDragging) { @@ -1192,8 +1160,6 @@ function setupMobileToggle() { $(document).on('mouseup', function(e) { if (!mouseDown) return; - console.log('[RPG Mobile] >>> MOUSEUP EVENT FIRED <<<', { isDragging }); - mouseDown = false; if (isDragging) { @@ -1209,6 +1175,9 @@ function setupMobileToggle() { 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 setTimeout(() => { $mobileToggle.css('transition', ''); @@ -1231,13 +1200,9 @@ function setupMobileToggle() { // Touch end - save position or toggle panel $mobileToggle.on('touchend', function(e) { - console.log('[RPG Mobile] >>> TOUCHEND EVENT FIRED <<<', { isDragging }); - // TEMPORARILY COMMENTED FOR DIAGNOSIS - might be blocking click fallback // e.preventDefault(); - console.log('[RPG Mobile] Touch end details:', { isDragging }); - if (isDragging) { // Was dragging - save new position const offset = $mobileToggle.offset(); @@ -1251,6 +1216,9 @@ function setupMobileToggle() { console.log('[RPG Mobile] Saved new FAB position:', newPosition); + // Constrain to viewport bounds (now that position is saved) + setTimeout(() => constrainFabToViewport(), 10); + // Re-enable transitions setTimeout(() => { $mobileToggle.css('transition', ''); @@ -1397,6 +1365,9 @@ function setupMobileToggle() { } wasMobile = isMobile; + + // Constrain FAB to viewport after resize (only if user has positioned it) + constrainFabToViewport(); }, 150); // Debounce only for mobile→desktop }); @@ -1423,6 +1394,77 @@ function setupMobileToggle() { } } +/** + * 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). From 60e371c726d1c6f61e47e57fb599e9d978d3fb8b Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Thu, 16 Oct 2025 11:25:38 +1100 Subject: [PATCH 05/16] fix: reposition 'Show thoughts' button above avatar on mobile Previously, the thought icon button was positioned to the left of the character avatar on mobile, causing it to appear partially off-screen due to lack of left padding around avatars. Changes: - Add mobile-specific positioning logic in index.js to detect viewport width <= 1000px and calculate centered horizontal position - Add CSS transform in mobile media query to shift icon 50px right and 45px upward from calculated position - Center thought panel horizontally on mobile when opened - Add debug logging to verify mobile detection Result: On mobile, the thought icon now appears above and to the right of the avatar, fully visible and accessible. --- index.js | 27 ++++++++++++++++++++++++++- style.css | 11 +++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index b03fe29..8958ff0 100644 --- a/index.js +++ b/index.js @@ -3554,7 +3554,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 diff --git a/style.css b/style.css index 639f762..f553756 100644 --- a/style.css +++ b/style.css @@ -3392,6 +3392,17 @@ body:has(.rpg-panel.rpg-position-left) #sheld { justify-content: center; padding: 0; } + + /* ======================================== + 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; + } } /* Extra small screens - adjust FAB position */ From e342f4d1002a600b0e9e3508532422720cd4caf3 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Thu, 16 Oct 2025 11:33:59 +1100 Subject: [PATCH 06/16] fix: improve mobile UX for relationship badge and thought panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed two mobile UI issues: 1. Relationship badge sizing on mobile: - Badge was stretching vertically due to .rpg-editable mobile styles - Override padding, min-height, and line-height for badge on mobile - Keep badge at compact 18px × 18px to prevent covering avatar 2. Thought panel initial state: - Panel was showing by default instead of the icon - Fixed initialization: hide panel, show icon - Users now click the 💭 icon to open the dialogue as intended Changes ensure proper mobile experience with appropriately sized UI elements and correct initial visibility states. --- index.js | 5 +++-- style.css | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 8958ff0..e9f8ea5 100644 --- a/index.js +++ b/index.js @@ -3647,8 +3647,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 }); diff --git a/style.css b/style.css index f553756..3f0498a 100644 --- a/style.css +++ b/style.css @@ -3403,6 +3403,20 @@ body:has(.rpg-panel.rpg-position-left) #sheld { /* Use transform to shift icon above and to the right of avatar */ transform: translate(50px, -45px) !important; } + + /* ======================================== + 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 */ From 9219fe3f19cb870dad35b27c39e54970575d0439 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Thu, 16 Oct 2025 11:39:15 +1100 Subject: [PATCH 07/16] feat: add smooth animation to mobile FAB drag behavior Improved the mobile floating action button (FAB) drag experience with smooth, performant animations: - Use requestAnimationFrame to throttle position updates during drag - Add will-change CSS property to optimize rendering performance - Add dragging class to disable transitions during active drag - Change cursor to grab/grabbing for better visual feedback - Remove janky direct CSS updates in favor of RAF-based updates Technical improvements: - Position updates now synced to display refresh rate (~60fps) - Prevents layout thrashing from excessive DOM manipulation - Smooth transition animations when drag ends Result: Dragging the FAB button now feels fluid and responsive on mobile devices instead of laggy and jumpy. --- index.js | 56 +++++++++++++++++++++++++++++++++++-------------------- style.css | 11 +++++++++-- 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/index.js b/index.js index e9f8ea5..f3e92fb 100644 --- a/index.js +++ b/index.js @@ -1032,6 +1032,24 @@ function setupMobileToggle() { 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) { @@ -1059,7 +1077,7 @@ function setupMobileToggle() { // Start dragging if held long enough OR moved far enough if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) { isDragging = true; - $mobileToggle.css('transition', 'none'); // Disable transitions while dragging + $mobileToggle.addClass('dragging'); // Disable transitions while dragging } if (isDragging) { @@ -1082,13 +1100,12 @@ function setupMobileToggle() { newX = Math.max(minX, Math.min(maxX, newX)); newY = Math.max(minY, Math.min(maxY, newY)); - // Apply position - $mobileToggle.css({ - left: newX + 'px', - top: newY + 'px', - right: 'auto', - bottom: 'auto' - }); + // Store pending position and request animation frame for smooth update + pendingX = newX; + pendingY = newY; + if (!rafId) { + rafId = requestAnimationFrame(updateFabPosition); + } } }); @@ -1123,7 +1140,7 @@ function setupMobileToggle() { // Start dragging if held long enough OR moved far enough if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) { isDragging = true; - $mobileToggle.css('transition', 'none'); + $mobileToggle.addClass('dragging'); // Disable transitions while dragging } if (isDragging) { @@ -1146,13 +1163,12 @@ function setupMobileToggle() { newX = Math.max(minX, Math.min(maxX, newX)); newY = Math.max(minY, Math.min(maxY, newY)); - // Apply position - $mobileToggle.css({ - left: newX + 'px', - top: newY + 'px', - right: 'auto', - bottom: 'auto' - }); + // Store pending position and request animation frame for smooth update + pendingX = newX; + pendingY = newY; + if (!rafId) { + rafId = requestAnimationFrame(updateFabPosition); + } } }); @@ -1178,9 +1194,9 @@ function setupMobileToggle() { // Constrain to viewport bounds (now that position is saved) setTimeout(() => constrainFabToViewport(), 10); - // Re-enable transitions + // Re-enable transitions with smooth animation setTimeout(() => { - $mobileToggle.css('transition', ''); + $mobileToggle.removeClass('dragging'); }, 50); isDragging = false; @@ -1219,9 +1235,9 @@ function setupMobileToggle() { // Constrain to viewport bounds (now that position is saved) setTimeout(() => constrainFabToViewport(), 10); - // Re-enable transitions + // Re-enable transitions with smooth animation setTimeout(() => { - $mobileToggle.css('transition', ''); + $mobileToggle.removeClass('dragging'); }, 50); isDragging = false; diff --git a/style.css b/style.css index 3f0498a..4d7a3bf 100644 --- a/style.css +++ b/style.css @@ -2942,12 +2942,19 @@ body:has(.rpg-panel.rpg-position-left) #sheld { border: 2px solid var(--SmartThemeBorderColor); color: var(--rpg-text, #ecf0f1); font-size: 1.25rem; - cursor: pointer; + cursor: grab; z-index: 10002; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - transition: opacity 0.3s ease, transform 0.2s 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 { From aef9cf812da9f784fc41cccaa997466cac6ede45 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Thu, 16 Oct 2025 12:16:43 +1100 Subject: [PATCH 08/16] feat: add smooth animation to thought icon scroll tracking Improved thought icon behavior on mobile with smooth 60fps animations matching the FAB button drag experience: CSS changes: - Add transition properties for top/left position changes - Use 0.2s ease-out timing for smooth, natural movement - Add will-change: top, left for browser rendering optimization - Applied in mobile media query (@media max-width: 1000px) JavaScript changes: - Wrap position updates in requestAnimationFrame() - Cancel pending RAF before scheduling new update (debouncing) - Sync position updates with display refresh rate - Same pattern as FAB button smooth drag implementation Technical details: - RAF throttling prevents layout thrashing - CSS transitions handle the actual animation - Combined approach gives 60fps smooth tracking - Icon follows avatar smoothly during scroll on mobile Result: Thought icon smoothly tracks avatar position during scroll instead of jumping around, with buttery smooth 60fps animation. --- index.js | 91 +++++++++++++++++++++++++++++++------------------------ style.css | 3 ++ 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/index.js b/index.js index f3e92fb..762cfe9 100644 --- a/index.js +++ b/index.js @@ -3692,7 +3692,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(); @@ -3700,47 +3703,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 4d7a3bf..c20e960 100644 --- a/style.css +++ b/style.css @@ -3409,6 +3409,9 @@ body:has(.rpg-panel.rpg-position-left) #sheld { #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; } /* ======================================== From c756704abcdb2c0f83e5a1676402399e9cf38e6f Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Thu, 16 Oct 2025 12:17:43 +1100 Subject: [PATCH 09/16] chore: simplify mobile tab label from 'Info & Characters' to 'Info' --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 762cfe9..befeccc 100644 --- a/index.js +++ b/index.js @@ -1514,7 +1514,7 @@ function setupMobileTabs() { } // Combine Info and Characters into one tab if (hasInfoOrCharacters) { - tabs.push(''); + tabs.push(''); } const $tabNav = $('
' + tabs.join('') + '
'); From a6bc0211489de7c1c814b45329e0078979eff2a9 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Thu, 16 Oct 2025 12:29:40 +1100 Subject: [PATCH 10/16] refactor: use display toggle instead of transform for mobile panel Replaced transform-based off-screen positioning with display toggle for cleaner, simpler mobile panel behavior: CSS changes: - Replace transform: translateX(100%) with display: none (closed state) - Panel completely removed from layout when closed - Add display: block when rpg-mobile-open class applied - Use CSS animation for smooth slide-in effect on open - Create @keyframes rpgSlideInFromRight for 0.3s ease-in-out Benefits: 1. Panel doesn't affect viewport/layout when closed 2. No need for overflow-x: hidden hacks 3. Simpler implementation - just toggle display property 4. Better performance - browser doesn't track hidden element 5. Still has smooth slide-in animation via CSS @keyframes JavaScript: - No changes needed - already toggles rpg-mobile-open class correctly - Display changes happen automatically via CSS Trade-off: - Slide-out animation on close is instant (could add if desired) - But this is acceptable and matches many mobile UX patterns Result: Mobile panel cleanly appears/disappears without viewport issues or layout side effects. Much simpler and more robust solution. --- style.css | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/style.css b/style.css index c20e960..6d4747d 100644 --- a/style.css +++ b/style.css @@ -3036,9 +3036,8 @@ body:has(.rpg-panel.rpg-position-left) #sheld { max-width: 400px !important; height: calc(100dvh - var(--topBarBlockSize)) !important; - /* Start off-screen to the right */ - transform: translateX(100%) !important; - transition: transform 0.3s ease-in-out !important; + /* Hidden by default - completely removed from layout */ + display: none !important; overflow-y: auto !important; -webkit-overflow-scrolling: touch; @@ -3051,10 +3050,21 @@ body:has(.rpg-panel.rpg-position-left) #sheld { box-shadow: -5px 0 20px var(--rpg-shadow); } - /* Show panel when opened - slides in from right */ + /* Show panel when opened with slide-in animation */ .rpg-panel.rpg-mobile-open { - transform: translateX(0) !important; + display: block !important; z-index: 50; + animation: rpgSlideInFromRight 0.3s ease-in-out; + } + + /* Slide-in animation from right */ + @keyframes rpgSlideInFromRight { + from { + transform: translateX(100%); + } + to { + transform: translateX(0); + } } /* Disable collapsed state on mobile */ From 3d32a04d57d1834ed09b8ac6a22f1c65de65710a Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Thu, 16 Oct 2025 12:33:07 +1100 Subject: [PATCH 11/16] feat: add smooth slide-out animation for mobile panel closing Added matching slide-out animation when closing the mobile panel to match the smooth slide-in animation on opening: CSS changes (style.css): - Add .rpg-mobile-closing class with rpgSlideOutToRight animation - Create @keyframes rpgSlideOutToRight (0.3s ease-in-out) - Panel slides smoothly off-screen to right before hiding JavaScript changes (index.js): - Add closeMobilePanelWithAnimation() helper function - Use animationend event to wait for animation before hiding - Replace all instant removeClass() calls with helper function - Maintains smooth 0.3s animation timing matching slide-in Implementation details: - Helper function adds .rpg-mobile-closing class - Waits for CSS animation to complete via animationend event - Then removes closing class and overlay - All close operations now use this helper for consistency Result: Panel now smoothly slides in AND out with matching 0.3s animations. No more jarring instant disappearance on close. --- index.js | 53 +++++++++++++++++++++++++++++------------------------ style.css | 17 +++++++++++++++++ 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/index.js b/index.js index befeccc..56b6716 100644 --- a/index.js +++ b/index.js @@ -985,6 +985,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). */ @@ -1246,10 +1264,8 @@ function setupMobileToggle() { console.log('[RPG Mobile] Quick tap detected - toggling panel'); if ($panel.hasClass('rpg-mobile-open')) { - // Close panel - $panel.removeClass('rpg-mobile-open'); - $overlay.remove(); - $mobileToggle.removeClass('active'); + // Close panel with animation + closeMobilePanelWithAnimation(); } else { // Open panel $panel.addClass('rpg-mobile-open'); @@ -1258,9 +1274,7 @@ function setupMobileToggle() { // Close when clicking overlay $overlay.on('click', function() { - $panel.removeClass('rpg-mobile-open'); - $overlay.remove(); - $mobileToggle.removeClass('active'); + closeMobilePanelWithAnimation(); }); } } @@ -1283,9 +1297,7 @@ function setupMobileToggle() { // Work on both mobile and desktop (removed viewport check) if ($panel.hasClass('rpg-mobile-open')) { console.log('[RPG Mobile] Click: Closing panel'); - $panel.removeClass('rpg-mobile-open'); - $overlay.remove(); - $mobileToggle.removeClass('active'); + closeMobilePanelWithAnimation(); } else { console.log('[RPG Mobile] Click: Opening panel'); $panel.addClass('rpg-mobile-open'); @@ -1294,9 +1306,7 @@ function setupMobileToggle() { $overlay.on('click', function() { console.log('[RPG Mobile] Overlay clicked - closing panel'); - $panel.removeClass('rpg-mobile-open'); - $overlay.remove(); - $mobileToggle.removeClass('active'); + closeMobilePanelWithAnimation(); }); } }); @@ -1322,10 +1332,8 @@ function setupMobileToggle() { // Clear collapsed state - mobile doesn't use collapse $panel.removeClass('rpg-collapsed'); - // Close panel on mobile - CSS handles smooth transition - $panel.removeClass('rpg-mobile-open'); - $mobileToggle.removeClass('active'); - $('.rpg-mobile-overlay').remove(); + // Close panel on mobile with animation + closeMobilePanelWithAnimation(); // Clear any inline styles that might be overriding CSS $panel.attr('style', ''); @@ -1360,7 +1368,7 @@ function setupMobileToggle() { // Disable transitions to prevent left→right slide animation $panel.css('transition', 'none'); - $panel.removeClass('rpg-mobile-open'); + $panel.removeClass('rpg-mobile-open rpg-mobile-closing'); $mobileToggle.removeClass('active'); $('.rpg-mobile-overlay').remove(); @@ -1646,11 +1654,9 @@ function setupCollapseToggle() { }); if (isOpen) { - // Close panel + // Close panel with animation console.log('[RPG Mobile] Closing panel'); - $panel.removeClass('rpg-mobile-open'); - $('.rpg-mobile-overlay').remove(); - $('#rpg-mobile-toggle').removeClass('active'); + closeMobilePanelWithAnimation(); } else { // Open panel console.log('[RPG Mobile] Opening panel'); @@ -1673,8 +1679,7 @@ function setupCollapseToggle() { // Close when clicking overlay $overlay.on('click', function() { console.log('[RPG Mobile] Overlay clicked - closing panel'); - $panel.removeClass('rpg-mobile-open'); - $overlay.remove(); + closeMobilePanelWithAnimation(); updateCollapseToggleIcon(); }); } diff --git a/style.css b/style.css index 6d4747d..13fdfdd 100644 --- a/style.css +++ b/style.css @@ -3057,6 +3057,13 @@ body:has(.rpg-panel.rpg-position-left) #sheld { 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 { @@ -3067,6 +3074,16 @@ body:has(.rpg-panel.rpg-position-left) #sheld { } } + /* Slide-out animation to right */ + @keyframes rpgSlideOutToRight { + from { + transform: translateX(0); + } + to { + transform: translateX(100%); + } + } + /* Disable collapsed state on mobile */ .rpg-panel.rpg-collapsed { max-width: 100dvw !important; From 7971056440bd1dc4405ab39bbd2be86de0c7eaa9 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Thu, 16 Oct 2025 13:41:34 +1100 Subject: [PATCH 12/16] refactor: complete professional redesign of dice roller modal for mobile BREAKING CHANGES: Dice roller now uses modern ES6 class architecture Features: - Mobile-first CSS with fluid responsive units (clamp, min, max) - ES6 DiceModal class with proper state management (IDLE, ROLLING, SHOWING_RESULT) - Semantic HTML with ARIA attributes for accessibility - CSS state classes (.is-open, .is-closing, .is-animating) - Touch-friendly 44px minimum tap targets - Desktop enhancement with @media (min-width: 1001px) Fixes: - Fixed mobile viewport overflow with min-height: 0 on flex children - Reduced max-height to 70vh for guaranteed mobile fit - Removed all jQuery .show()/.hide() inline style injections - Removed !important CSS hacks from mobile media query - Fixed transparent modal background (80% opaque neutral gray) - Darkened backdrop overlay (85% opaque black) Technical: - Backdrop uses ::before pseudo-element (no wrapper div needed) - Flattened HTML structure with proper semantic elements - Backwards compatible wrapper functions preserved - Grid layout for inputs (stacked mobile, side-by-side desktop) - Proper CSS specificity hierarchy (no !important needed) - Removed .rpg-dice-popup-overlay div dependency Accessibility: - role="dialog" with aria-modal="true" - aria-labelledby for dialog title - aria-live regions for dynamic content - aria-busy for loading states - Proper focus management on open/close --- index.js | 221 ++++++++++++++++---- style.css | 543 +++++++++++++++++++++++++++++--------------------- template.html | 42 ++-- 3 files changed, 515 insertions(+), 291 deletions(-) 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 @@ - -