- Add THEME_FIX_PLAN.md: comprehensive plan for theme system consistency - Add ADMIN_SIDEBAR_PLAN.md: reusable admin sidebar component plan - Add WOOD_PANELING_PLAN.md: wood paneling and full-width layout plan
21 KiB
Wood Paneling & Full-Width Layout Implementation
Problem Statement
The current "wood themes" were implemented as color themes with CSS gradients, but they should be wood paneling textures applied to the dashboard bookshelf background only. Additionally, the app uses constrained width containers (max-w-7xl) which limit screen real estate on large monitors, unlike modern bookshelf apps like Audiobookshelf.
Root Cause Analysis
Current Implementation Issues
-
Wood themes are misimplemented:
- Wood themes (
wood-light,wood-dark,wood-mahogany) are in the theme dropdown alongside color themes - Applied via CSS gradients in
header.tschangeThemeTo() - Gradients applied to entire
<body>instead of just bookshelf background - Not integrated with
theme.tsapplyTheme()function
- Wood themes (
-
Constrained layout:
- Header uses
max-w-7xl mx-autocontainer - Dashboard content uses
max-w-7xl mx-autocontainer - Other pages use
max-w-7xl mx-autocontainers - Wastes horizontal space on large monitors
- Header uses
-
No active indicators:
- Theme dropdown doesn't show which theme/wood option is currently active
- Users can't tell what's selected without remembering
Solution Strategy
Approach: Separate Concerns
Principle: Wood paneling is a visual treatment for the bookshelf, not a color theme. It should be:
- Separate from color themes
- Applied only to
#collections-containeron dashboard - Stored in localStorage (not database)
- User-selectable from theme dropdown (but sectioned separately)
Layout principle: Full-width layout for modern app feel, better space utilization.
Implementation Plan
Phase 0: Download Wood Textures
Source URLs:
-
Light Wood: https://opengameart.org/content/light-wood-1024x1024
- Author: qubodup
- License: CC0 (no attribution required)
- File: qubodup-light_wood.png (1.9 MB)
- Resolution: 1024x1024
-
Dark & Mahogany Wood: https://opengameart.org/content/5-wood-textures
- Author: Luke.RUSTLTD
- License: CC0 (no attribution required)
- File: wood_0.zip (6.5 MB) - contains 5 textures
- Select 2 best: darkest and medium/reddish
- Resolution: Likely 512x512 or 1024x1024
Download Steps:
# Create textures directory
mkdir -p web/static/textures
# Download light wood
cd web/static/textures
curl -O https://opengameart.org/sites/default/files/qubodup-light_wood.png
mv qubodup-light_wood.png wood-light.png
# Download texture pack (dark & mahogany)
cd /tmp
curl -O https://opengameart.org/sites/default/files/wood_0.zip
unzip wood_0.zip
# Select and copy best 2 textures (you'll need to review and choose)
# Copy selected ones to web/static/textures/wood-dark.png
# Copy selected ones to web/static/textures/wood-mahogany.png
Manual Steps:
- Extract
wood_0.zip - Review the 5 textures
- Select darkest → rename to
wood-dark.png - Select medium/reddish → rename to
wood-mahogany.png - Move both to
web/static/textures/
Optional Optimization:
# Use ImageOptim (Mac) or FileOptimizer (Windows/Linux)
# Or use online: TinyPNG.com
# Goal: Keep each under 500 KB for fast loading
Final Files:
web/static/textures/wood-light.pngweb/static/textures/wood-dark.pngweb/static/textures/wood-mahogany.png
Phase 1: Remove Wood Themes from Theme System
Goal: Remove wood themes as color themes from the dropdown
Files to Modify:
1. templates/header.templ
- Remove wood theme buttons (lines 86-99)
- This section will be replaced with wood paneling section in Phase 3
2. tailwind.config.ts
- Remove
theme-wood-light,theme-wood-dark,theme-wood-mahoganyfrom safelist (lines 21-23)
Before:
safelist: [
'theme-wood-light',
'theme-wood-dark',
'theme-wood-mahogany',
// ... other themes
]
After:
safelist: [
// Remove wood theme entries
// Keep only color themes
]
3. web/src/header.ts
- Remove wood theme handling from
changeThemeTo()function (lines 31-51) - Keep only regular theme handling
Before:
const changeThemeTo = (theme: string): void => {
if (theme.startsWith('wood-')) {
// Apply wood background theme
document.body.className = `theme-${theme}`;
// Set wood background style
let woodGradient = '';
switch (theme) {
case 'wood-light':
woodGradient = 'linear-gradient(135deg, #deb887 0%, #d2a679 50%, #c9975b 100%)';
break;
case 'wood-dark':
woodGradient = 'linear-gradient(135deg, #8b7355 0%, #6b5344 50%, #5a4636 100%)';
break;
case 'wood-mahogany':
woodGradient = 'linear-gradient(135deg, #a0522d 0%, #8b4513 50%, #7a3c10 100%)';
break;
}
document.body.style.background = woodGradient;
document.body.style.backgroundSize = 'cover';
document.body.style.backgroundAttachment = 'fixed';
} else {
// Apply regular theme
document.body.className = `theme-${theme}`;
document.body.style.background = '';
document.body.style.backgroundSize = '';
document.body.style.backgroundAttachment = '';
}
// ... rest of function
};
After:
const changeThemeTo = (theme: string): void => {
// Apply regular theme only
document.body.className = `theme-${theme}`;
document.body.style.background = '';
document.body.style.backgroundSize = '';
document.body.style.backgroundAttachment = '';
// Save to localStorage
localStorage.setItem('theme', theme);
// Save to server if logged in
const token = localStorage.getItem('token');
if (token) {
fetch('/api/auth/theme', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ theme })
}).catch(err => console.log('Theme save failed', err));
}
// Close dropdown
const dropdown = document.getElementById('theme-dropdown');
if (dropdown) {
dropdown.classList.add('hidden');
}
};
Phase 2: Create Wood Paneling System
Goal: Create separate wood paneling preference system (localStorage only)
New File: web/src/woodPaneling.ts
// Wood paneling management functionality
type WoodPanelingType = 'none' | 'wood-light' | 'wood-dark' | 'wood-mahogany';
const WOOD_STORAGE_KEY = 'wood-paneling';
// Apply wood paneling to collections container
const applyWoodPaneling = (paneling: WoodPanelingType): void => {
const container = document.getElementById('collections-container');
if (!container) return;
// Remove all wood background classes
container.classList.remove('bg-wood-light', 'bg-wood-dark', 'bg-wood-mahogany');
if (paneling !== 'none') {
// Add selected wood background class
container.classList.add(`bg-${paneling}`);
}
// Save to localStorage
localStorage.setItem(WOOD_STORAGE_KEY, paneling);
};
// Load wood paneling from localStorage on page load
const loadWoodPaneling = (): void => {
const stored = localStorage.getItem(WOOD_STORAGE_KEY) as WoodPanelingType | null;
if (stored) {
applyWoodPaneling(stored);
} else {
// Default to none
applyWoodPaneling('none');
}
};
// Change wood paneling (called from theme dropdown)
const changeWoodPaneling = (paneling: WoodPanelingType): void => {
applyWoodPaneling(paneling);
// Update active indicators
updateWoodPanelingIndicators();
// Close dropdown
const dropdown = document.getElementById('theme-dropdown');
if (dropdown) {
dropdown.classList.add('hidden');
}
};
// Update visual indicators for wood paneling buttons
const updateWoodPanelingIndicators = (): void => {
const currentWood = localStorage.getItem(WOOD_STORAGE_KEY) || 'none';
// Update wood paneling buttons
document.querySelectorAll('.wood-paneling-btn').forEach(btn => {
const wood = btn.getAttribute('data-wood');
if (wood === currentWood) {
// Active state - darker background
btn.style.backgroundColor = 'var(--bg-primary)';
} else {
// Inactive state
btn.style.backgroundColor = 'var(--bg-secondary)';
}
});
};
// Make functions available globally
(window as any).changeWoodPaneling = changeWoodPaneling;
(window as any).loadWoodPaneling = loadWoodPaneling;
(window as any).updateWoodPanelingIndicators = updateWoodPanelingIndicators;
// Auto-initialize when DOM is ready
if (typeof document !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
loadWoodPaneling();
updateWoodPanelingIndicators();
});
} else {
loadWoodPaneling();
updateWoodPanelingIndicators();
}
}
Build: Add to build system (will be compiled to web/static/woodPaneling.js)
Phase 3: Update Header with Wood Paneling Section
Goal: Replace wood theme buttons with wood paneling section
File: templates/header.templ
Replace wood theme section (lines 86-99) with:
<div class="border-t pt-2 mt-2" style="border-color: var(--border);">
<p class="text-xs mb-2" style="color: var(--text-secondary)">Bookshelf Background</p>
<button onclick="changeWoodPaneling('none')"
class="wood-paneling-btn w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
style="color: var(--text-primary);"
data-wood="none">
None
</button>
<button onclick="changeWoodPaneling('wood-light')"
class="wood-paneling-btn w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
style="color: var(--text-primary);"
data-wood="wood-light">
<span class="inline-block w-4 h-4 rounded mr-2"
style="background: url('/static/textures/wood-light.png'); background-size: cover;"></span>
Wood Light
</button>
<button onclick="changeWoodPaneling('wood-dark')"
class="wood-paneling-btn w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
style="color: var(--text-primary);"
data-wood="wood-dark">
<span class="inline-block w-4 h-4 rounded mr-2"
style="background: url('/static/textures/wood-dark.png'); background-size: cover;"></span>
Wood Dark
</button>
<button onclick="changeWoodPaneling('wood-mahogany')"
class="wood-paneling-btn w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
style="color: var(--text-primary);"
data-wood="wood-mahogany">
<span class="inline-block w-4 h-4 rounded mr-2"
style="background: url('/static/textures/wood-mahogany.png'); background-size: cover;"></span>
Wood Mahogany
</button>
</div>
Add script includes at bottom of header (before closing script tag):
<script src="/static/woodPaneling.js"></script>
Phase 4: Add Active Indicators for Themes
Goal: Show which theme and wood paneling option is active
New File: web/src/themeDropdown.ts
// Theme dropdown active indicator management
// Update visual indicators for theme buttons
const updateThemeIndicators = (): void => {
const currentTheme = localStorage.getItem('theme') || 'tokyo-night';
// Update theme buttons (all buttons with changeThemeTo onclick)
document.querySelectorAll('[onclick^="changeThemeTo"]').forEach(btn => {
const onclick = btn.getAttribute('onclick') || '';
const match = onclick.match(/changeThemeTo\('(.+?)'\)/);
if (match) {
const theme = match[1];
if (theme === currentTheme) {
// Active state - darker background
btn.style.backgroundColor = 'var(--bg-primary)';
} else {
// Inactive state
btn.style.backgroundColor = 'var(--bg-secondary)';
}
}
});
};
// Make function available globally
(window as any).updateThemeIndicators = updateThemeIndicators;
// Update on dropdown toggle
const originalToggleThemeDropdown = (window as any).toggleThemeDropdown;
if (originalToggleThemeDropdown) {
(window as any).toggleThemeDropdown = () => {
originalToggleThemeDropdown();
updateThemeIndicators();
(window as any).updateWoodPanelingIndicators?.();
};
}
// Update after theme changes
const originalChangeThemeTo = (window as any).changeThemeTo;
if (originalChangeThemeTo) {
(window as any).changeThemeTo = (...args: unknown[]) => {
originalChangeThemeTo(...args);
updateThemeIndicators();
};
}
// Auto-initialize when DOM is ready
if (typeof document !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', updateThemeIndicators);
} else {
updateThemeIndicators();
}
}
Add script include to templates/header.templ:
<script src="/static/themeDropdown.js"></script>
Phase 5: Update Tailwind Config for Wood Backgrounds
Goal: Add Tailwind utility classes for wood textures
File: tailwind.config.ts
Add to theme.extend:
export default {
// ... existing config
theme: {
extend: {
// ... existing extensions
backgroundImage: {
'wood-light': "url('/static/textures/wood-light.png')",
'wood-dark': "url('/static/textures/wood-dark.png')",
'wood-mahogany': "url('/static/textures/wood-mahogany.png')",
},
},
},
};
Regenerate CSS:
npm run build:css
# or whatever the build command is
Phase 6: Apply Full-Width Layout Globally
Goal: Remove max-w-7xl containers from header and all pages
Files to Modify:
1. templates/header.templ
Line 5 - Remove max-w-7xl:
<!-- Before -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- After -->
<div class="w-full px-4 sm:px-6 lg:px-8">
2. templates/dashboard.templ
Line 27 - Library selector:
<!-- Before -->
<div class="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
<!-- After -->
<div class="w-full px-4 py-3 flex items-center justify-between">
Line 67 - Collections container:
<!-- Before -->
<main id="collections-container" class="max-w-7xl mx-auto px-4 py-8">
<!-- After -->
<main id="collections-container" class="w-full px-4 py-8">
3. Other pages (remove max-w-7xl mx-auto from all content containers):
templates/collections.templtemplates/progress.templtemplates/queue.templtemplates/devices.templtemplates/analytics.templtemplates/conflicts.templtemplates/unlinked_books.templtemplates/bookshelf.templtemplates/profile.templtemplates/admin.templ(see note below)templates/admin_library.templ(see note below)templates/admin_users.templ(see note below)templates/docs.templ
Pattern: Replace class="max-w-7xl mx-auto px-4 ..." with class="w-full px-4 ..."
Note: Keep padding (px-4, py-8, etc.) for readability.
Special note for ALL admin templates (admin.templ, admin_library.templ, admin_users.templ): These files were already modified by ADMIN_SIDEBAR_PLAN.md to add sidebar layout wrappers. The current structure for all three is:
<div class="flex min-h-screen" style="background-color: var(--bg-primary)">
@AdminSidebar(user, currentPath)
<main class="flex-1 p-8">
<div class="max-w-7xl">
(or <div class="max-w-6xl"> in some cases)
To apply full-width layout, change the inner div in all three:
<!-- Before -->
<div class="max-w-7xl">
(or <div class="max-w-6xl">)
<!-- After -->
<div class="w-full">
Do NOT modify the main element or the flex wrapper in any admin template - these are part of the admin sidebar layout.
Phase 7: Add Inline Script for Dashboard Initialization
Goal: Ensure wood paneling is applied immediately on dashboard load
File: templates/dashboard.templ
Add inline script after existing scripts (around line 20):
<script>
// Apply wood paneling immediately (before DOM fully loads)
(function() {
const woodPaneling = localStorage.getItem('wood-paneling') || 'none';
if (woodPaneling !== 'none') {
document.addEventListener('DOMContentLoaded', function() {
const container = document.getElementById('collections-container');
if (container) {
container.classList.add('bg-' + woodPaneling);
}
});
}
})();
</script>
This ensures:
- Wood paneling applied before external woodPaneling.js loads
- No flash of wrong background
- Progressive enhancement: works even if JS fails
Testing Checklist
After implementation:
Wood Paneling
- Wood paneling options appear in theme dropdown under "Bookshelf Background" section
- "None" option removes wood texture
- Selecting "Wood Light" applies light wood texture to dashboard background
- Selecting "Wood Dark" applies dark wood texture to dashboard background
- Selecting "Wood Mahogany" applies mahogany texture to dashboard background
- Texture covers only
#collections-containerarea (not header or sidebar) - Texture looks seamless when repeated
- Wood preference persists across page refreshes (localStorage)
- Active wood option has darker background highlight
- Switching between wood options updates immediately
Full-Width Layout
- Header uses full width on all pages
- Dashboard uses full width
- Collections page uses full width
- Bookshelf page uses full width
- All other pages use full width
- Content still has appropriate padding for readability
- Layout responsive on mobile devices
Theme System
- Wood themes removed from theme dropdown
- Color themes still work correctly
- Active color theme has darker background highlight
- Theme switching still saves to server
- No wood theme options in color theme section
Performance
- Wood textures load quickly (no visible delay on reasonable connection)
- No flash of wrong background on page load
- Texture files are optimized (< 500 KB each)
Files Summary
New Files Created
web/src/woodPaneling.ts- Wood paneling managementweb/src/themeDropdown.ts- Active indicator managementweb/static/textures/wood-light.png- Light wood textureweb/static/textures/wood-dark.png- Dark wood textureweb/static/textures/wood-mahogany.png- Mahogany texture
Files Modified
templates/header.templ- Remove wood themes, add wood paneling section, remove max-widthtailwind.config.ts- Remove theme-wood-* from safelist, add wood background imagesweb/src/header.ts- Remove wood theme logic fromchangeThemeTo()templates/dashboard.templ- Remove max-width, add inline initialization scripttemplates/*.templ- Removemax-w-7xlfrom all page templates
Migration Notes
Breaking Changes
- Wood themes removed from theme system
- Users with wood theme selected will see tokyo-night (or their last color theme)
- Wood paneling preference starts as "none" (users must opt-in)
Backward Compatibility
- Color themes unaffected
- localStorage theme preference still works
- Server-side theme sync still works
Database Dependencies
- None (wood paneling is localStorage only)
Performance
- Wood textures only loaded when selected
- "None" option = no texture load (fastest)
- Texture size optimized to ~200-500 KB each
Progressive Enhancement
- Pages work without wood paneling (default "none")
- Wood paneling degrades gracefully if JS fails
- Color themes work independently of wood paneling
Rollback Plan
If issues arise:
- Remove wood paneling section from header
- Restore
max-w-7xlcontainers in templates - Delete wood texture files
- Revert header.ts to include wood theme logic
- Wood paneling preference in localStorage will be ignored (harmless)
Future Improvements
- Per-page wood paneling: Allow wood on bookshelf page, not just dashboard
- Texture variety: Add more wood options (oak, pine, walnut)
- Texture intensity: Add opacity slider for subtler effect
- Custom textures: Allow users to upload their own backgrounds
- Server-side sync: Store wood paneling preference in database for cross-device sync
- Preview mode: Show texture preview before applying
- High-DPI textures: Provide 2x versions for retina displays
Attribution
All wood textures are CC0 licensed (no attribution required):
- Light wood: qubodup - https://opengameart.org/content/light-wood-1024x1024
- Dark/Mahogany wood: Luke.RUSTLTD - https://opengameart.org/content/5-wood-textures
Optional: Add to README.md or about page for documentation purposes.