From 8df6548e0b4ab4478c7adf297ac2f394d4685925 Mon Sep 17 00:00:00 2001 From: Spicy_Marinara Date: Sat, 3 Jan 2026 11:40:07 +0100 Subject: [PATCH] v2.1.3 - Improved thought bubble positioning and responsiveness - Align thought bubbles with avatar top instead of center for better visibility - Fix issue where bubbles extend above avatar when scrolling is limited - Change thought circles to horizontal layout for cleaner visual flow - Add responsive positioning that adapts to screen width changes - Implement smart viewport detection to prevent cutoff at narrow widths --- README.md | 7 ++ manifest.json | 2 +- src/systems/rendering/thoughts.js | 110 ++++++++++++++++-------------- style.css | 14 ++-- 4 files changed, 73 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index c84db85..a284ef0 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,13 @@ An immersive RPG extension for browsers that tracks character stats, scene infor ## 🆕 What's New +### v2.1.3 +- **Improved Thought Bubble Positioning**: Thought bubbles now align with the top of the character's avatar/name +- **Better Visibility**: Fixed issue where thought bubbles would extend above the avatar when scrolling is limited +- **Horizontal Thought Circles**: Thought circles now display horizontally for a cleaner visual flow +- **Responsive Positioning**: Thought bubbles dynamically adjust to screen width changes and stay visible at all resolutions +- **Smart Viewport Detection**: Bubbles automatically reposition to avoid being cut off at narrow window widths + ### v2.1.2 - Added optional toggle for Relationship Status Fields in Edit Trackers - Relationship fields and emoji badges can now be disabled/enabled like other trackers diff --git a/manifest.json b/manifest.json index 049c402..4976567 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Marysia", - "version": "2.1.2", + "version": "2.1.3", "homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern" } diff --git a/src/systems/rendering/thoughts.js b/src/systems/rendering/thoughts.js index 3e6ee14..901bb73 100644 --- a/src/systems/rendering/thoughts.js +++ b/src/systems/rendering/thoughts.js @@ -1142,7 +1142,7 @@ export function createThoughtPanel($message, thoughtsArray) { const panelWidth = 350; const panelMargin = 20; - let top = avatarRect.top + (avatarRect.height / 2); + let top = avatarRect.top; // Align top of panel with top of avatar let left; let right; let useRightPosition = false; @@ -1176,50 +1176,47 @@ export function createThoughtPanel($message, thoughtsArray) { }); } 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 const chatContainer = $('#chat')[0]; const chatRect = chatContainer ? chatContainer.getBoundingClientRect() : { right: window.innerWidth }; - // Position bubble starting from chat edge, extending right - left = chatRect.right + panelMargin; // Start at chat's right edge + margin - useRightPosition = false; // Use left positioning so it extends right - iconLeft = chatRect.right + 10; // Icon just at the chat edge + // Calculate how much space is available to the right of the chat + const spaceRight = window.innerWidth - chatRect.right; + + // If there's enough space, position normally; otherwise, adjust + if (spaceRight >= panelWidth + panelMargin * 2) { + left = chatRect.right + panelMargin; + } else { + // Not enough space on the right, position closer to chat or slightly overlapping + left = Math.max(chatRect.right - panelWidth - panelMargin, window.innerWidth - panelWidth - 10); + } + + useRightPosition = false; + iconLeft = chatRect.right + 10; $thoughtPanel.addClass('rpg-thought-panel-right'); $thoughtIcon.addClass('rpg-thought-icon-right'); - // Position circles to flow from left (toward chat/avatar) to right (toward panel) - $thoughtPanel.find('.rpg-thought-circles').css({ - top: 'calc(50% - 50px)', - left: '-25px', - bottom: 'auto', - right: 'auto' - }); - // Mirror the circle flow for right side (left-to-right) - $thoughtPanel.find('.rpg-thought-circles').css('align-items', 'flex-start'); - $thoughtPanel.find('.rpg-circle-1').css({ 'align-self': 'flex-start', 'margin-right': '0', 'margin-left': '0' }); - $thoughtPanel.find('.rpg-circle-2').css({ 'align-self': 'flex-start', 'margin-right': '0', 'margin-left': '4px' }); - $thoughtPanel.find('.rpg-circle-3').css({ 'align-self': 'flex-start', 'margin-right': '0', 'margin-left': '8px' }); + // Circles use default CSS positioning } else { // Main panel is on right, so thought bubble goes on left (near avatar) - left = avatarRect.left - panelWidth - panelMargin; - iconLeft = avatarRect.left - 40; + const chatContainer = $('#chat')[0]; + const chatRect = chatContainer ? chatContainer.getBoundingClientRect() : { left: 0 }; + + // Calculate how much space is available to the left of the chat + const spaceLeft = chatRect.left; + + // If there's enough space, position normally; otherwise, adjust + if (spaceLeft >= panelWidth + panelMargin * 2) { + left = avatarRect.left - panelWidth - panelMargin; + } else { + // Not enough space on the left, position it within visible area + left = Math.max(10, avatarRect.left - panelWidth - panelMargin); + } + + iconLeft = Math.max(10, avatarRect.left - 40); $thoughtPanel.addClass('rpg-thought-panel-left'); $thoughtIcon.addClass('rpg-thought-icon-left'); - // Position circles to flow from avatar (left) to bubble (more left) - // Circles should flow right-to-left when bubble is on left - $thoughtPanel.find('.rpg-thought-circles').css({ - top: 'calc(50% - 50px)', - right: '-25px', - bottom: 'auto', - left: 'auto' - }); - // Keep the circle flow for left side (right-to-left) - default from CSS - $thoughtPanel.find('.rpg-thought-circles').css('align-items', 'flex-end'); - $thoughtPanel.find('.rpg-circle-1').css({ 'align-self': 'flex-end', 'margin-left': '0', 'margin-right': '0' }); - $thoughtPanel.find('.rpg-circle-2').css({ 'align-self': 'flex-end', 'margin-left': '0', 'margin-right': '4px' }); - $thoughtPanel.find('.rpg-circle-3').css({ 'align-self': 'flex-end', 'margin-left': '0', 'margin-right': '8px' }); + // Circles use default CSS positioning } if (useRightPosition) { @@ -1298,34 +1295,47 @@ export function createThoughtPanel($message, thoughtsArray) { // Schedule update on next frame positionUpdateRaf = requestAnimationFrame(() => { const newAvatarRect = $avatar[0].getBoundingClientRect(); - const newTop = newAvatarRect.top + (newAvatarRect.height / 2); + const newTop = newAvatarRect.top; // Align with avatar top const newIconTop = newAvatarRect.top; let newLeft, newIconLeft; - if (panelPosition === 'left') { + const isMobileNow = window.innerWidth <= 1000; + + if (isMobileNow) { + newLeft = window.innerWidth / 2 - panelWidth / 2; + newIconLeft = newAvatarRect.left + (newAvatarRect.width / 2) - 18; + } else 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; + const spaceRight = window.innerWidth - chatRect.right; - $thoughtPanel.css({ - top: `${newTop}px`, - left: `${newLeft}px`, - right: 'auto' - }); + if (spaceRight >= panelWidth + panelMargin * 2) { + newLeft = chatRect.right + panelMargin; + } else { + newLeft = Math.max(chatRect.right - panelWidth - panelMargin, window.innerWidth - panelWidth - 10); + } + newIconLeft = chatRect.right + 10; } else { // Left position relative to avatar - newLeft = newAvatarRect.left - panelWidth - panelMargin; - newIconLeft = newAvatarRect.left - 40; + const chatContainer = $('#chat')[0]; + const chatRect = chatContainer ? chatContainer.getBoundingClientRect() : { left: 0 }; + const spaceLeft = chatRect.left; - $thoughtPanel.css({ - top: `${newTop}px`, - left: `${newLeft}px`, - right: 'auto' - }); + if (spaceLeft >= panelWidth + panelMargin * 2) { + newLeft = newAvatarRect.left - panelWidth - panelMargin; + } else { + newLeft = Math.max(10, newAvatarRect.left - panelWidth - panelMargin); + } + newIconLeft = Math.max(10, newAvatarRect.left - 40); } + $thoughtPanel.css({ + top: `${newTop}px`, + left: `${newLeft}px`, + right: 'auto' + }); + $thoughtIcon.css({ top: `${newIconTop}px`, left: `${newIconLeft}px`, diff --git a/style.css b/style.css index e7a9d59..57a7e73 100644 --- a/style.css +++ b/style.css @@ -4458,7 +4458,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld { z-index: 1000; /* Lower z-index to stay below dropdown menus */ pointer-events: auto; max-width: 15.3rem; /* 30% smaller than 21.875rem (350px → 245px) */ - transform: translateY(-50%); + transform: translateY(0); /* No vertical centering - align with avatar top */ animation: thoughtPanelFadeIn 0.4s ease-out; } @@ -4528,9 +4528,10 @@ body:has(.rpg-panel.rpg-position-left) #sheld { .rpg-thought-circles { position: absolute; display: flex; - flex-direction: column-reverse; /* Reverse so circles go upward */ - align-items: flex-end; /* Align to the right side */ - gap: 0.75em; + flex-direction: row-reverse; /* Horizontal layout, flipped */ + align-items: center; /* Center vertically */ + gap: 0.5em; + inset: 0px -50px auto; z-index: 1; } @@ -4545,23 +4546,18 @@ body:has(.rpg-panel.rpg-position-left) #sheld { width: 0.5rem; height: 0.5rem; animation-delay: 0s; - align-self: flex-end; /* Circle 1 on the far right (at avatar) */ } .rpg-circle-2 { width: 0.75rem; height: 0.75rem; animation-delay: 0.2s; - align-self: flex-end; - margin-right: 4px; /* Move slightly left from circle 1 */ } .rpg-circle-3 { width: 1rem; height: 1rem; animation-delay: 0.4s; - align-self: flex-end; - margin-right: 8px; /* Move more left from circle 1 */ } /* Thought bubble main container */