feat(ui): add tab navigation system for desktop and mobile
Desktop (2 tabs): - Status tab: User Stats + Info Box + Character Thoughts - Inventory tab: Inventory system (dedicated space) Mobile (3 tabs): - Stats tab: User Stats only - Info tab: Info Box + Character Thoughts - Inventory tab: Inventory only Features: - Created desktop.js module for desktop tab management - Updated mobile.js to use 3-tab structure (more breathing room on small screens) - Added CSS styling for desktop tabs (hover states, active indicators) - Implemented viewport transition handlers (desktop ↔ mobile) - Tabs replace dividers (cleaner visual separation) - Character thoughts can now expand to fill vertical space This resolves the cramped 4-section panel issue by organizing content into logical tabs on both desktop and mobile.
This commit is contained in:
@@ -100,6 +100,10 @@ import {
|
|||||||
setupMobileKeyboardHandling,
|
setupMobileKeyboardHandling,
|
||||||
setupContentEditableScrolling
|
setupContentEditableScrolling
|
||||||
} from './src/systems/ui/mobile.js';
|
} from './src/systems/ui/mobile.js';
|
||||||
|
import {
|
||||||
|
setupDesktopTabs,
|
||||||
|
removeDesktopTabs
|
||||||
|
} from './src/systems/ui/desktop.js';
|
||||||
|
|
||||||
// Feature modules
|
// Feature modules
|
||||||
import { setupPlotButtons, sendPlotProgression } from './src/systems/features/plotProgression.js';
|
import { setupPlotButtons, sendPlotProgression } from './src/systems/features/plotProgression.js';
|
||||||
@@ -368,6 +372,11 @@ async function initUI() {
|
|||||||
// Setup mobile toggle button
|
// Setup mobile toggle button
|
||||||
setupMobileToggle();
|
setupMobileToggle();
|
||||||
|
|
||||||
|
// Setup desktop tabs (only on desktop viewport)
|
||||||
|
if (window.innerWidth > 1000) {
|
||||||
|
setupDesktopTabs();
|
||||||
|
}
|
||||||
|
|
||||||
// Setup collapse/expand toggle button
|
// Setup collapse/expand toggle button
|
||||||
setupCollapseToggle();
|
setupCollapseToggle();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* Desktop UI Module
|
||||||
|
* Handles desktop-specific UI functionality: tab navigation
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up desktop tab navigation for organizing content.
|
||||||
|
* Only runs on desktop viewports (>1000px).
|
||||||
|
* Creates two tabs: Status (Stats/Info/Thoughts) and Inventory.
|
||||||
|
*/
|
||||||
|
export function setupDesktopTabs() {
|
||||||
|
const isDesktop = window.innerWidth > 1000;
|
||||||
|
if (!isDesktop) return;
|
||||||
|
|
||||||
|
// Check if tabs already exist
|
||||||
|
if ($('.rpg-tabs-nav').length > 0) return;
|
||||||
|
|
||||||
|
const $contentBox = $('.rpg-content-box');
|
||||||
|
|
||||||
|
// Get existing sections
|
||||||
|
const $userStats = $('#rpg-user-stats');
|
||||||
|
const $infoBox = $('#rpg-info-box');
|
||||||
|
const $thoughts = $('#rpg-thoughts');
|
||||||
|
const $inventory = $('#rpg-inventory');
|
||||||
|
|
||||||
|
// If no sections exist, nothing to organize
|
||||||
|
if ($userStats.length === 0 && $infoBox.length === 0 && $thoughts.length === 0 && $inventory.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create tab navigation
|
||||||
|
const $tabNav = $(`
|
||||||
|
<div class="rpg-tabs-nav">
|
||||||
|
<button class="rpg-tab-btn active" data-tab="status">
|
||||||
|
<i class="fa-solid fa-chart-simple"></i>
|
||||||
|
<span>Status</span>
|
||||||
|
</button>
|
||||||
|
<button class="rpg-tab-btn" data-tab="inventory">
|
||||||
|
<i class="fa-solid fa-box"></i>
|
||||||
|
<span>Inventory</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create tab content containers
|
||||||
|
const $statusTab = $('<div class="rpg-tab-content active" data-tab-content="status"></div>');
|
||||||
|
const $inventoryTab = $('<div class="rpg-tab-content" data-tab-content="inventory"></div>');
|
||||||
|
|
||||||
|
// Move sections into their respective tabs (detach to preserve event handlers)
|
||||||
|
if ($userStats.length > 0) {
|
||||||
|
$statusTab.append($userStats.detach());
|
||||||
|
$userStats.show();
|
||||||
|
}
|
||||||
|
if ($infoBox.length > 0) {
|
||||||
|
$statusTab.append($infoBox.detach());
|
||||||
|
$infoBox.show();
|
||||||
|
}
|
||||||
|
if ($thoughts.length > 0) {
|
||||||
|
$statusTab.append($thoughts.detach());
|
||||||
|
$thoughts.show();
|
||||||
|
}
|
||||||
|
if ($inventory.length > 0) {
|
||||||
|
$inventoryTab.append($inventory.detach());
|
||||||
|
$inventory.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide dividers on desktop tabs (tabs separate content naturally)
|
||||||
|
$('.rpg-divider').hide();
|
||||||
|
|
||||||
|
// Build desktop tab structure
|
||||||
|
const $tabsContainer = $('<div class="rpg-tabs-container"></div>');
|
||||||
|
$tabsContainer.append($tabNav);
|
||||||
|
$tabsContainer.append($statusTab);
|
||||||
|
$tabsContainer.append($inventoryTab);
|
||||||
|
|
||||||
|
// Replace content box with tabs container
|
||||||
|
$contentBox.html('').append($tabsContainer);
|
||||||
|
|
||||||
|
// Handle tab switching
|
||||||
|
$tabNav.find('.rpg-tab-btn').on('click', function() {
|
||||||
|
const tabName = $(this).data('tab');
|
||||||
|
|
||||||
|
// Update active tab button
|
||||||
|
$tabNav.find('.rpg-tab-btn').removeClass('active');
|
||||||
|
$(this).addClass('active');
|
||||||
|
|
||||||
|
// Update active tab content
|
||||||
|
$('.rpg-tab-content').removeClass('active');
|
||||||
|
$(`.rpg-tab-content[data-tab-content="${tabName}"]`).addClass('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[RPG Desktop] Desktop tabs initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes desktop tab navigation and restores original layout.
|
||||||
|
* Used when transitioning from desktop to mobile.
|
||||||
|
*/
|
||||||
|
export function removeDesktopTabs() {
|
||||||
|
// Get sections from tabs before removing
|
||||||
|
const $userStats = $('#rpg-user-stats').detach();
|
||||||
|
const $infoBox = $('#rpg-info-box').detach();
|
||||||
|
const $thoughts = $('#rpg-thoughts').detach();
|
||||||
|
const $inventory = $('#rpg-inventory').detach();
|
||||||
|
|
||||||
|
// Remove tabs container
|
||||||
|
$('.rpg-tabs-container').remove();
|
||||||
|
|
||||||
|
// Get dividers
|
||||||
|
const $dividerStats = $('#rpg-divider-stats');
|
||||||
|
const $dividerInfo = $('#rpg-divider-info');
|
||||||
|
const $dividerThoughts = $('#rpg-divider-thoughts');
|
||||||
|
|
||||||
|
// Restore original sections to content box in correct order
|
||||||
|
const $contentBox = $('.rpg-content-box');
|
||||||
|
|
||||||
|
// Re-insert sections in original order: User Stats, Info Box, Thoughts, Inventory
|
||||||
|
if ($dividerStats.length) {
|
||||||
|
$dividerStats.before($userStats);
|
||||||
|
$dividerInfo.before($infoBox);
|
||||||
|
$dividerThoughts.before($thoughts);
|
||||||
|
$contentBox.append($inventory);
|
||||||
|
} else {
|
||||||
|
// Fallback if dividers don't exist
|
||||||
|
$contentBox.append($userStats);
|
||||||
|
$contentBox.append($infoBox);
|
||||||
|
$contentBox.append($thoughts);
|
||||||
|
$contentBox.append($inventory);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show sections and dividers
|
||||||
|
$userStats.show();
|
||||||
|
$infoBox.show();
|
||||||
|
$thoughts.show();
|
||||||
|
$inventory.show();
|
||||||
|
$('.rpg-divider').show();
|
||||||
|
|
||||||
|
console.log('[RPG Desktop] Desktop tabs removed');
|
||||||
|
}
|
||||||
+37
-24
@@ -6,6 +6,7 @@
|
|||||||
import { extensionSettings } from '../../core/state.js';
|
import { extensionSettings } from '../../core/state.js';
|
||||||
import { saveSettings } from '../../core/persistence.js';
|
import { saveSettings } from '../../core/persistence.js';
|
||||||
import { closeMobilePanelWithAnimation, updateCollapseToggleIcon } from './layout.js';
|
import { closeMobilePanelWithAnimation, updateCollapseToggleIcon } from './layout.js';
|
||||||
|
import { setupDesktopTabs, removeDesktopTabs } from './desktop.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the mobile toggle button (FAB) with drag functionality.
|
* Sets up the mobile toggle button (FAB) with drag functionality.
|
||||||
@@ -331,6 +332,9 @@ export function setupMobileToggle() {
|
|||||||
if (!wasMobile && isMobile) {
|
if (!wasMobile && isMobile) {
|
||||||
console.log('[RPG Mobile] Transitioning desktop -> mobile');
|
console.log('[RPG Mobile] Transitioning desktop -> mobile');
|
||||||
|
|
||||||
|
// Remove desktop tabs first
|
||||||
|
removeDesktopTabs();
|
||||||
|
|
||||||
// Remove desktop positioning classes
|
// Remove desktop positioning classes
|
||||||
$panel.removeClass('rpg-position-right rpg-position-left rpg-position-top');
|
$panel.removeClass('rpg-position-right rpg-position-left rpg-position-top');
|
||||||
|
|
||||||
@@ -384,6 +388,9 @@ export function setupMobileToggle() {
|
|||||||
// Remove mobile tabs structure
|
// Remove mobile tabs structure
|
||||||
removeMobileTabs();
|
removeMobileTabs();
|
||||||
|
|
||||||
|
// Setup desktop tabs
|
||||||
|
setupDesktopTabs();
|
||||||
|
|
||||||
// Force reflow to apply position instantly
|
// Force reflow to apply position instantly
|
||||||
$panel[0].offsetHeight;
|
$panel[0].offsetHeight;
|
||||||
|
|
||||||
@@ -519,54 +526,59 @@ export function setupMobileTabs() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create tab navigation (only show tabs for sections that exist)
|
// Create tab navigation (3 tabs for mobile)
|
||||||
const tabs = [];
|
const tabs = [];
|
||||||
const hasInfoOrCharacters = $infoBox.length > 0 || $thoughts.length > 0;
|
const hasStats = $userStats.length > 0;
|
||||||
const hasStatsOrInventory = $userStats.length > 0 || $inventory.length > 0;
|
const hasInfo = $infoBox.length > 0 || $thoughts.length > 0;
|
||||||
|
const hasInventory = $inventory.length > 0;
|
||||||
|
|
||||||
if (hasStatsOrInventory) {
|
// Tab 1: Stats (User Stats only)
|
||||||
|
if (hasStats) {
|
||||||
tabs.push('<button class="rpg-mobile-tab active" data-tab="stats"><i class="fa-solid fa-chart-bar"></i><span>Stats</span></button>');
|
tabs.push('<button class="rpg-mobile-tab active" data-tab="stats"><i class="fa-solid fa-chart-bar"></i><span>Stats</span></button>');
|
||||||
}
|
}
|
||||||
// Combine Info and Characters into one tab
|
// Tab 2: Info (Info Box + Character Thoughts)
|
||||||
if (hasInfoOrCharacters) {
|
if (hasInfo) {
|
||||||
tabs.push('<button class="rpg-mobile-tab ' + (tabs.length === 0 ? 'active' : '') + '" data-tab="info-characters"><i class="fa-solid fa-book"></i><span>Info</span></button>');
|
tabs.push('<button class="rpg-mobile-tab ' + (tabs.length === 0 ? 'active' : '') + '" data-tab="info"><i class="fa-solid fa-book"></i><span>Info</span></button>');
|
||||||
|
}
|
||||||
|
// Tab 3: Inventory
|
||||||
|
if (hasInventory) {
|
||||||
|
tabs.push('<button class="rpg-mobile-tab ' + (tabs.length === 0 ? 'active' : '') + '" data-tab="inventory"><i class="fa-solid fa-box"></i><span>Inventory</span></button>');
|
||||||
}
|
}
|
||||||
|
|
||||||
const $tabNav = $('<div class="rpg-mobile-tabs">' + tabs.join('') + '</div>');
|
const $tabNav = $('<div class="rpg-mobile-tabs">' + tabs.join('') + '</div>');
|
||||||
|
|
||||||
// Determine which tab should be active
|
// Determine which tab should be active
|
||||||
let firstTab = '';
|
let firstTab = '';
|
||||||
if (hasStatsOrInventory) firstTab = 'stats';
|
if (hasStats) firstTab = 'stats';
|
||||||
else if (hasInfoOrCharacters) firstTab = 'info-characters';
|
else if (hasInfo) firstTab = 'info';
|
||||||
|
else if (hasInventory) firstTab = 'inventory';
|
||||||
|
|
||||||
// Create tab content wrappers
|
// Create tab content wrappers
|
||||||
const $statsTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'stats' ? 'active' : '') + '" data-tab-content="stats"></div>');
|
const $statsTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'stats' ? 'active' : '') + '" data-tab-content="stats"></div>');
|
||||||
const $infoCharactersTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'info-characters' ? 'active' : '') + '" data-tab-content="info-characters"></div>');
|
const $infoTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'info' ? 'active' : '') + '" data-tab-content="info"></div>');
|
||||||
|
const $inventoryTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'inventory' ? 'active' : '') + '" data-tab-content="inventory"></div>');
|
||||||
// Create combined content wrapper for Info and Characters
|
|
||||||
const $combinedWrapper = $('<div class="rpg-mobile-combined-content"></div>');
|
|
||||||
|
|
||||||
// Move sections into their respective tabs (detach to preserve event handlers)
|
// Move sections into their respective tabs (detach to preserve event handlers)
|
||||||
|
// Stats tab: User Stats only
|
||||||
if ($userStats.length > 0) {
|
if ($userStats.length > 0) {
|
||||||
$statsTab.append($userStats.detach());
|
$statsTab.append($userStats.detach());
|
||||||
$userStats.show();
|
$userStats.show();
|
||||||
}
|
}
|
||||||
if ($inventory.length > 0) {
|
|
||||||
$statsTab.append($inventory.detach());
|
// Info tab: Info Box + Character Thoughts
|
||||||
$inventory.show();
|
|
||||||
}
|
|
||||||
if ($infoBox.length > 0) {
|
if ($infoBox.length > 0) {
|
||||||
$combinedWrapper.append($infoBox.detach());
|
$infoTab.append($infoBox.detach());
|
||||||
$infoBox.show();
|
$infoBox.show();
|
||||||
}
|
}
|
||||||
if ($thoughts.length > 0) {
|
if ($thoughts.length > 0) {
|
||||||
$combinedWrapper.append($thoughts.detach());
|
$infoTab.append($thoughts.detach());
|
||||||
$thoughts.show();
|
$thoughts.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add combined wrapper to the info-characters tab
|
// Inventory tab: Inventory only
|
||||||
if (hasInfoOrCharacters) {
|
if ($inventory.length > 0) {
|
||||||
$infoCharactersTab.append($combinedWrapper);
|
$inventoryTab.append($inventory.detach());
|
||||||
|
$inventory.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide dividers on mobile
|
// Hide dividers on mobile
|
||||||
@@ -577,8 +589,9 @@ export function setupMobileTabs() {
|
|||||||
$mobileContainer.append($tabNav);
|
$mobileContainer.append($tabNav);
|
||||||
|
|
||||||
// Only append tab content wrappers that have content
|
// Only append tab content wrappers that have content
|
||||||
if (hasStatsOrInventory) $mobileContainer.append($statsTab);
|
if (hasStats) $mobileContainer.append($statsTab);
|
||||||
if (hasInfoOrCharacters) $mobileContainer.append($infoCharactersTab);
|
if (hasInfo) $mobileContainer.append($infoTab);
|
||||||
|
if (hasInventory) $mobileContainer.append($inventoryTab);
|
||||||
|
|
||||||
// Insert mobile tab structure at the beginning of content box
|
// Insert mobile tab structure at the beginning of content box
|
||||||
$contentBox.prepend($mobileContainer);
|
$contentBox.prepend($mobileContainer);
|
||||||
|
|||||||
@@ -4109,6 +4109,81 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
DESKTOP TABS SYSTEM
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Desktop tabs container */
|
||||||
|
.rpg-tabs-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop tab navigation */
|
||||||
|
.rpg-tabs-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
background: var(--SmartThemeBlurTintColor);
|
||||||
|
border-bottom: 2px solid var(--SmartThemeBorderColor);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop tab button */
|
||||||
|
.rpg-tab-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-tab-btn:hover {
|
||||||
|
background: var(--SmartThemeQuoteColor);
|
||||||
|
color: var(--ac-style-color-matchedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-tab-btn.active {
|
||||||
|
background: var(--SmartThemeQuoteColor);
|
||||||
|
border-bottom-color: var(--ac-style-color-matchedText);
|
||||||
|
color: var(--ac-style-color-matchedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-tab-btn i {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop tab content */
|
||||||
|
.rpg-tab-content {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-tab-content.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide dividers when tabs are active (tabs separate content) */
|
||||||
|
.rpg-tabs-container .rpg-divider {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Mobile Responsive Styles */
|
/* Mobile Responsive Styles */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.rpg-inventory-subtabs {
|
.rpg-inventory-subtabs {
|
||||||
|
|||||||
Reference in New Issue
Block a user