feat: implement ultra-compact mode for extreme narrow widths

Problem:
At ~254px panel width, even 4 "priority" buttons (Lock, Edit, Tracker
Settings, Hamburger) overflow the visible area, making buttons inaccessible
and taking space from tabs.

Root Cause:
Priority buttons were always visible regardless of width, but at extreme
narrow widths there isn't room for both tabs and multiple buttons.

The Solution:
Introduced 4th responsive mode: "Ultra-Compact Mode" (<400px)

New Responsive Breakpoints:
- Full Mode: > 900px (all buttons visible)
- Overflow Mode: 700-900px (priority + "More" menu ⋮)
- Compact Mode: 400-700px (priority + hamburger menu ☰)
- Ultra-Compact Mode: < 400px (HAMBURGER MENU ONLY ☰)

Implementation:
1. Added ultraCompactModeWidth: 400px threshold
2. Implemented setUltraCompactMode() that hides ALL buttons except hamburger
3. Fixed buildDropdownMenu() to correctly include priority buttons when
   includeAll=true (was missing in compact mode)
4. Updated handleResize() to detect ultra-compact threshold
5. All buttons remain accessible via comprehensive hamburger menu

Benefits:
 Maximizes space for tabs at narrow widths (only 1 button)
 All functionality accessible via hamburger menu
 Follows mobile-first design patterns
 Graceful degradation through 4 responsive tiers
 No horizontal scrolling needed

Fixes: Buttons becoming non-clickable at narrow panel widths
This commit is contained in:
Lucas 'Paperboy' Rose-Winters
2025-11-04 17:27:47 +11:00
parent 2c86af5561
commit 1c8c4af32d
+77 -13
View File
@@ -1,10 +1,11 @@
/**
* Header Overflow Manager
*
* Manages responsive button overflow behavior with three modes:
* Manages responsive button overflow behavior with four modes:
* - Full Mode (>900px): All buttons visible
* - Overflow Mode (500-900px): Priority buttons + "More" menu
* - Compact Mode (<500px): Priority buttons + Hamburger menu
* - Overflow Mode (700-900px): Priority buttons + "More" menu
* - Compact Mode (400-700px): Priority buttons + Hamburger menu
* - Ultra-Compact Mode (<400px): Hamburger menu ONLY
*
* Uses ResizeObserver for accurate width detection and smooth transitions.
*/
@@ -18,7 +19,8 @@ export class HeaderOverflowManager {
this.headerContainer = headerContainer;
this.options = {
fullModeWidth: 900, // px
compactModeWidth: 500, // px
compactModeWidth: 700, // px
ultraCompactModeWidth: 400, // px - New breakpoint for extreme narrowness
debounceDelay: 100, // ms
...options
};
@@ -111,7 +113,9 @@ export class HeaderOverflowManager {
handleResize(width) {
let newMode = 'full';
if (width < this.options.compactModeWidth) {
if (width < this.options.ultraCompactModeWidth) {
newMode = 'ultraCompact';
} else if (width < this.options.compactModeWidth) {
newMode = 'compact';
} else if (width < this.options.fullModeWidth) {
newMode = 'overflow';
@@ -143,6 +147,9 @@ export class HeaderOverflowManager {
case 'compact':
this.setCompactMode();
break;
case 'ultraCompact':
this.setUltraCompactMode();
break;
}
}
@@ -150,6 +157,14 @@ export class HeaderOverflowManager {
* Full Mode: Show all buttons except menu-only
*/
setFullMode() {
// Show priority buttons
this.priorityButtons.forEach(btn => {
const inlineStyle = btn.getAttribute('style');
if (!inlineStyle || !inlineStyle.includes('display: none')) {
btn.style.display = '';
}
});
// Show all overflow buttons except menu-only ones
this.overflowButtons.forEach(btn => {
// Menu-only buttons always stay hidden (managed by menu)
@@ -176,6 +191,14 @@ export class HeaderOverflowManager {
* Overflow Mode: Priority buttons + "More" menu
*/
setOverflowMode() {
// Ensure priority buttons are visible
this.priorityButtons.forEach(btn => {
const inlineStyle = btn.getAttribute('style');
if (!inlineStyle || !inlineStyle.includes('display: none')) {
btn.style.display = '';
}
});
// Hide overflow buttons (will be in dropdown)
// Store original visibility before hiding
this.overflowButtons.forEach(btn => {
@@ -201,10 +224,16 @@ export class HeaderOverflowManager {
* Compact Mode: Priority buttons + Hamburger menu
*/
setCompactMode() {
// Ensure priority buttons are visible
this.priorityButtons.forEach(btn => {
const inlineStyle = btn.getAttribute('style');
if (!inlineStyle || !inlineStyle.includes('display: none')) {
btn.style.display = '';
}
});
// Hide all overflow buttons
// Store original visibility before hiding
this.overflowButtons.forEach(btn => {
// Menu-only buttons are always available in menu
if (btn.classList.contains('rpg-menu-only-btn')) {
btn.dataset.wasVisible = 'true';
} else {
@@ -218,7 +247,37 @@ export class HeaderOverflowManager {
this.overflowMenuBtn.style.display = 'none';
this.hamburgerMenuBtn.style.display = '';
// Build menu with all buttons (including visible ones for context)
// Build menu with all buttons (priority + overflow)
this.buildDropdownMenu(true);
}
/**
* Ultra-Compact Mode: Hamburger menu ONLY
*/
setUltraCompactMode() {
// Hide priority buttons
this.priorityButtons.forEach(btn => {
const computedStyle = window.getComputedStyle(btn);
btn.dataset.wasVisible = computedStyle.display !== 'none' ? 'true' : 'false';
btn.style.display = 'none';
});
// Hide all overflow buttons
this.overflowButtons.forEach(btn => {
if (btn.classList.contains('rpg-menu-only-btn')) {
btn.dataset.wasVisible = 'true';
} else {
const computedStyle = window.getComputedStyle(btn);
btn.dataset.wasVisible = computedStyle.display !== 'none' ? 'true' : 'false';
}
btn.style.display = 'none';
});
// Show hamburger menu button
this.overflowMenuBtn.style.display = 'none';
this.hamburgerMenuBtn.style.display = '';
// Build menu with ALL buttons
this.buildDropdownMenu(true);
}
@@ -229,8 +288,9 @@ export class HeaderOverflowManager {
buildDropdownMenu(includeAll) {
this.dropdownMenu.innerHTML = '';
// CORRECTED: When includeAll is true, combine priority and overflow buttons.
const buttonsToShow = includeAll
? [...this.overflowButtons]
? [...this.priorityButtons, ...this.overflowButtons]
: this.overflowButtons;
// Filter visible buttons (only include buttons that were visible before being hidden)
@@ -314,7 +374,9 @@ export class HeaderOverflowManager {
this.dropdownMenu.style.display = 'block';
// Update aria-expanded
const menuBtn = this.currentMode === 'compact' ? this.hamburgerMenuBtn : this.overflowMenuBtn;
const menuBtn = this.currentMode === 'compact' || this.currentMode === 'ultraCompact'
? this.hamburgerMenuBtn
: this.overflowMenuBtn;
menuBtn.setAttribute('aria-expanded', 'true');
// Add close listeners
@@ -376,7 +438,9 @@ export class HeaderOverflowManager {
e.preventDefault();
this.closeMenu();
// Return focus to menu button
const menuBtn = this.currentMode === 'compact' ? this.hamburgerMenuBtn : this.overflowMenuBtn;
const menuBtn = this.currentMode === 'compact' || this.currentMode === 'ultraCompact'
? this.hamburgerMenuBtn
: this.overflowMenuBtn;
menuBtn.focus();
break;
@@ -434,8 +498,8 @@ export class HeaderOverflowManager {
*/
refresh() {
console.log('[HeaderOverflowManager] Refreshing menu...');
if (this.currentMode === 'overflow' || this.currentMode === 'compact') {
this.buildDropdownMenu(this.currentMode === 'compact');
if (this.currentMode !== 'full') {
this.buildDropdownMenu(this.currentMode === 'compact' || this.currentMode === 'ultraCompact');
}
}