Files
bookhoard/WOOD_PANELING_PLAN.md
T
john-okeefe 79d10b5b37 docs: add theme fix and admin sidebar implementation plans
- 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
2026-02-24 10:24:05 -05:00

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

  1. 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.ts changeThemeTo()
    • Gradients applied to entire <body> instead of just bookshelf background
    • Not integrated with theme.ts applyTheme() function
  2. Constrained layout:

    • Header uses max-w-7xl mx-auto container
    • Dashboard content uses max-w-7xl mx-auto container
    • Other pages use max-w-7xl mx-auto containers
    • Wastes horizontal space on large monitors
  3. 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-container on 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:

  1. 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
  2. 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:

  1. Extract wood_0.zip
  2. Review the 5 textures
  3. Select darkest → rename to wood-dark.png
  4. Select medium/reddish → rename to wood-mahogany.png
  5. 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.png
  • web/static/textures/wood-dark.png
  • web/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-mahogany from 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.templ
  • templates/progress.templ
  • templates/queue.templ
  • templates/devices.templ
  • templates/analytics.templ
  • templates/conflicts.templ
  • templates/unlinked_books.templ
  • templates/bookshelf.templ
  • templates/profile.templ
  • templates/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-container area (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 management
  • web/src/themeDropdown.ts - Active indicator management
  • web/static/textures/wood-light.png - Light wood texture
  • web/static/textures/wood-dark.png - Dark wood texture
  • web/static/textures/wood-mahogany.png - Mahogany texture

Files Modified

  • templates/header.templ - Remove wood themes, add wood paneling section, remove max-width
  • tailwind.config.ts - Remove theme-wood-* from safelist, add wood background images
  • web/src/header.ts - Remove wood theme logic from changeThemeTo()
  • templates/dashboard.templ - Remove max-width, add inline initialization script
  • templates/*.templ - Remove max-w-7xl from 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:

  1. Remove wood paneling section from header
  2. Restore max-w-7xl containers in templates
  3. Delete wood texture files
  4. Revert header.ts to include wood theme logic
  5. Wood paneling preference in localStorage will be ignored (harmless)

Future Improvements

  1. Per-page wood paneling: Allow wood on bookshelf page, not just dashboard
  2. Texture variety: Add more wood options (oak, pine, walnut)
  3. Texture intensity: Add opacity slider for subtler effect
  4. Custom textures: Allow users to upload their own backgrounds
  5. Server-side sync: Store wood paneling preference in database for cross-device sync
  6. Preview mode: Show texture preview before applying
  7. High-DPI textures: Provide 2x versions for retina displays

Attribution

All wood textures are CC0 licensed (no attribution required):

Optional: Add to README.md or about page for documentation purposes.