Skip to content

Latest commit

 

History

History
725 lines (562 loc) · 16.4 KB

File metadata and controls

725 lines (562 loc) · 16.4 KB

Theme System

Overview

Pulsefeed supports two themes: Dark (default) and Light, implemented via Flask session storage and CSS class-based overrides.

How It Works

Session Storage

Location: Flask session in app.py

# Default to dark theme if not set
theme = session.get('theme', 'dark')

# Apply to all templates via context
@app.context_processor
def inject_theme():
    return {'theme': session.get('theme', 'dark')}

Body Class Application

All templates include theme class on body tag:

<body class="theme-{{ session.get('theme', 'dark') }}">
  <!-- Page content -->
</body>

This creates:

  • Dark theme: <body class="theme-dark"> (or no class, since dark is default)
  • Light theme: <body class="theme-light">

Theme Switching

Settings page: /settings route in app.py

@app.route('/settings', methods=['GET', 'POST'])
def settings():
    if request.method == 'POST':
        theme = request.form.get('theme', 'dark')
        session['theme'] = theme
        flash(f'Theme changed to {theme}', 'success')
        return redirect(request.referrer or url_for('index'))

    return render_template('settings.html')

Settings template: templates/settings.html

<form method="post">
  <div class="mb-3">
    <label class="form-label">Theme</label>
    <select name="theme" class="form-select">
      <option value="dark" {% if session.get('theme', 'dark') == 'dark' %}selected{% endif %}>
        Dark
      </option>
      <option value="light" {% if session.get('theme') == 'light' %}selected{% endif %}>
        Light
      </option>
    </select>
  </div>
  <button type="submit" class="btn btn-primary">Save Settings</button>
</form>

CSS Architecture

Location: static/styles.css

Two-Layer System

  1. Base styles (lines ~1-550): Apply to all elements, optimized for dark theme
  2. Light theme overrides (lines ~550+): .theme-light selector overrides for light theme

Pattern

/* Base dark theme styles */
.card {
  background: linear-gradient(135deg, #1a2642 0%, #0f1726 100%);
  border: 1px solid rgba(58, 93, 176, 0.3);
  color: #e6edf3;
}

/* Light theme overrides */
.theme-light .card {
  background: linear-gradient(135deg, #f0f4f8 0%, #e2e8f0 100%);
  border: 1px solid #cbd5e1;
  color: #1e293b;
}

All .theme-light overrides are grouped together in styles.css (around line 550+) for easy maintenance.

Styling Patterns

Cards and Containers

Dark theme:

background: linear-gradient(135deg, #1a2642 0%, #0f1726 100%);
border: 1px solid rgba(58, 93, 176, 0.3);
color: #e6edf3;

Light theme:

background: linear-gradient(135deg, #f0f4f8 0%, #e2e8f0 100%);
border: 1px solid #cbd5e1;
color: #1e293b;

Why gradients: Adds visual depth, makes cards feel elevated

Borders

Dark theme:

border: 1px solid rgba(58, 93, 176, 0.3);  /* Subtle blue tint */

Light theme:

border: 1px solid #cbd5e1;  /* Neutral gray */

Pattern: Borders should be slightly lighter than background for subtle definition

Shadows

Layered shadows for elevation:

box-shadow:
  0 4px 12px rgba(0, 0, 0, 0.15),  /* Larger, softer shadow */
  0 1px 3px rgba(0, 0, 0, 0.1);    /* Smaller, sharper shadow */

Why two shadows: Creates more realistic depth perception

Hover States

Pattern: Subtle background change with smooth transition

.button {
  background: #3a5db0;
  transition: background 0.2s ease;
}

.button:hover {
  background: #4a6dc0;  /* Slightly lighter */
}

Duration: 0.2s for instant feedback, 0.3s for larger transitions (like collapse animations)

Accessibility

Contrast Requirements

WCAG AA minimum: 4.5:1 for normal text, 3:1 for large text

Dark theme:

  • Background: #0d1117 (very dark)
  • Text: #e6edf3 (very light)
  • Contrast ratio: ~14:1 ✅

Light theme:

  • Background: #ffffff (white)
  • Text: #1e293b (very dark)
  • Contrast ratio: ~15:1 ✅

Form Labels

Always use high-contrast colors:

/* Dark theme */
.form-label {
  color: #e6edf3;
}

/* Light theme */
.theme-light .form-label {
  color: #1f2937;
}

Why critical: Low-contrast labels are a common accessibility violation

Adding New Themed Components

Step-by-Step Process

1. Write Dark Theme Styles (Main Section)

Add to main CSS section (~lines 1-550):

.new-component {
  background: linear-gradient(135deg, #1a2642 0%, #0f1726 100%);
  border: 1px solid rgba(58, 93, 176, 0.3);
  border-radius: 8px;
  padding: 1rem;
  color: #e6edf3;
  box-shadow:
    0 4px 12px rgba(0, 0, 0, 0.15),
    0 1px 3px rgba(0, 0, 0, 0.1);
}

.new-component:hover {
  background: linear-gradient(135deg, #1f2d4f 0%, #12192e 100%);
  transition: background 0.2s ease;
}

2. Add Light Theme Overrides

Add to .theme-light section (~lines 550+):

.theme-light .new-component {
  background: linear-gradient(135deg, #f0f4f8 0%, #e2e8f0 100%);
  border: 1px solid #cbd5e1;
  color: #1e293b;
  box-shadow:
    0 4px 12px rgba(0, 0, 0, 0.05),
    0 1px 3px rgba(0, 0, 0, 0.03);
}

.theme-light .new-component:hover {
  background: linear-gradient(135deg, #e5ebf1 0%, #d6dde6 100%);
}

3. Test Both Themes

Manual testing:

  1. Navigate to Settings page
  2. Switch to Light theme
  3. Navigate to page with new component
  4. Verify colors, contrast, and readability
  5. Test hover states and animations
  6. Switch back to Dark theme
  7. Repeat verification

Automated contrast checking:

  • Use browser DevTools > Accessibility panel
  • Check contrast ratios meet WCAG AA (4.5:1 minimum)

4. Commit Changes

git add static/styles.css
git commit -m "Add themed styles for [new-component]

- Add dark theme styles with gradient background
- Add light theme overrides for accessibility
- Ensure WCAG AA contrast compliance in both themes"

Category and Checklist Color System

User-Customizable Category Colors

Storage: Flask session (session['category_colors'])

DEFAULT_CATEGORY_COLORS = {
    "event": "#3b82f6",      # Blue
    "meeting": "#a855f7",    # Purple
    "reminder": "#f97316",   # Orange
    "birthday": "#ec4899",   # Pink
    "holiday": "#10b981"     # Green
}

def get_category_color(category: str) -> str:
    """Get color for a category from session or defaults."""
    category_colors = session.get('category_colors', {})
    return category_colors.get(category, DEFAULT_CATEGORY_COLORS.get(category, "#6b7280"))

Visual Radio Button Grids

Pattern: Replace dropdowns with visual button grids that use CSS custom properties for dynamic colors

.category-radio-btn {
  border-left: 4px solid var(--category-color, #6b7280);
  color: #e6edf3 !important;  /* IMPORTANT: !important needed to override Bootstrap */
}

HTML Usage:

<label class="btn category-radio-btn" for="catEvent"
       style="--category-color: {{ category_colors['event'] }}">
  <span class="category-emoji">📅</span>
  <span class="category-label">Event</span>
</label>

Why !important: Bootstrap button styles set text color on various states (:hover, :focus, :checked). Without !important, text can become unreadable when background darkens.

Checklist Button Grid

Same pattern applies to checklist selection:

<label class="btn checklist-radio-btn" for="checklist1"
       style="--checklist-color: {{ checklist.color or '#3a5db0' }}">
  <span class="checklist-label">{{ checklist.title }}</span>
</label>

Special "New Checklist" button: Uses dashed green border to visually distinguish from existing checklists.

.checklist-radio-btn.new-checklist {
  border-left-color: #22c55e;
  border-style: dashed;
}

Common Components and Their Patterns

Event and Task Cards

Pattern: Unified card components with colored left borders and hover animations

Dark theme:

.event-card, .task-card {
  background: rgba(59, 93, 176, 0.08) !important;
  border: 1px solid rgba(59, 93, 176, 0.2) !important;
  border-left: 4px solid var(--item-color, #3b82f6) !important;
  border-radius: 8px !important;
  padding: 0.875rem 1rem !important;
  transition: all 0.2s ease;
  cursor: pointer;
}

.event-card:hover, .task-card:hover {
  background: rgba(59, 93, 176, 0.12) !important;
  border-color: rgba(59, 93, 176, 0.35) !important;
  transform: translateX(4px);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}

Light theme:

.theme-light .event-card,
.theme-light .task-card {
  background: #ffffff !important;
  border: 1px solid #e5e7eb !important;
  border-left: 4px solid var(--item-color, #3b82f6) !important;
}

.theme-light .event-card:hover,
.theme-light .task-card:hover {
  background: #f9fafb !important;
  border-color: #d1d5db !important;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}

Usage in templates:

<!-- Events use category colors -->
<div class="event-card" data-category="meeting"
     style="--item-color: {{ get_category_color('meeting') }};">
  <!-- Event content -->
</div>

<!-- Tasks use checklist colors -->
<div class="task-card"
     style="--item-color: {{ checklist.color or '#3a5db0' }};">
  <!-- Task content -->
</div>

Important notes:

  • ALWAYS use CSS custom property --item-color for dynamic colors
  • Cards replace <li> list items - use <div class="p-3"> wrappers instead of <ul>
  • The render_item macro in _item_macros.html outputs cards, not list items
  • Color coding: Events use category colors, Tasks use checklist colors

Alert Boxes

Dark theme:

.alert-info {
  background: rgba(58, 93, 176, 0.2);  /* Semi-transparent blue */
  border: 1px solid rgba(58, 93, 176, 0.4);
  color: #e6edf3;
}

.alert-info strong {
  color: #93c5fd;  /* Lighter blue for emphasis */
}

Light theme:

.theme-light .alert-info {
  background: #dbeafe;  /* Solid light blue */
  border: 1px solid #93c5fd;
  color: #1e3a8a;  /* Dark blue text */
}

.theme-light .alert-info strong {
  color: #1e40af;  /* Darker blue for emphasis */
}

Buttons

Dark theme:

.btn-primary {
  background: #3a5db0;
  border: 1px solid #4a6dc0;
  color: #ffffff;
}

.btn-primary:hover {
  background: #4a6dc0;
  border: 1px solid #5a7dd0;
}

Light theme:

.theme-light .btn-primary {
  background: #3b82f6;
  border: 1px solid #2563eb;
  color: #ffffff;
}

.theme-light .btn-primary:hover {
  background: #2563eb;
  border: 1px solid #1d4ed8;
}

Form Inputs

Dark theme:

.form-control {
  background: #161b22;
  border: 1px solid #30363d;
  color: #e6edf3;
}

.form-control:focus {
  background: #0d1117;
  border-color: #3a5db0;
  box-shadow: 0 0 0 3px rgba(58, 93, 176, 0.3);
}

Light theme:

.theme-light .form-control {
  background: #ffffff;
  border: 1px solid #d1d5db;
  color: #1f2937;
}

.theme-light .form-control:focus {
  background: #ffffff;
  border-color: #3b82f6;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

Navigation Sidebar

Dark theme:

.sidebar {
  background: #0d1117;
  border-right: 1px solid #21262d;
}

.sidebar a {
  color: #8b949e;
}

.sidebar a:hover {
  background: rgba(58, 93, 176, 0.1);
  color: #e6edf3;
}

Light theme:

.theme-light .sidebar {
  background: #f9fafb;
  border-right: 1px solid #e5e7eb;
}

.theme-light .sidebar a {
  color: #6b7280;
}

.theme-light .sidebar a:hover {
  background: rgba(59, 130, 246, 0.1);
  color: #1f2937;
}

Color Palette Reference

Dark Theme Colors

Element Color Hex Usage
Background (darkest) Very dark gray #0d1117 Page background
Background (dark) Dark gray #161b22 Cards, inputs
Background (medium) Medium gray #21262d Borders, dividers
Primary blue Blue #3a5db0 Buttons, links
Primary blue (light) Light blue #93c5fd Highlights, badges
Text (primary) Very light gray #e6edf3 Body text
Text (secondary) Medium gray #8b949e Secondary text

Light Theme Colors

Element Color Hex Usage
Background (lightest) White #ffffff Page background
Background (light) Very light gray #f9fafb Cards, sidebar
Background (medium) Light gray #e5e7eb Borders, dividers
Primary blue Blue #3b82f6 Buttons, links
Primary blue (dark) Dark blue #1e40af Highlights, emphasis
Text (primary) Very dark gray #1f2937 Body text
Text (secondary) Medium gray #6b7280 Secondary text

File Organization

styles.css Structure

/* ==========================================
   Lines 1-50: Reset & Base Styles
   ========================================== */

/* ==========================================
   Lines 51-200: Layout (Grid, Flexbox)
   ========================================== */

/* ==========================================
   Lines 201-350: Components (Buttons, Forms)
   ========================================== */

/* ==========================================
   Lines 351-500: Page-Specific Styles
   ========================================== */

/* ==========================================
   Lines 501-550: Utilities & Helpers
   ========================================== */

/* ==========================================
   Lines 550+: LIGHT THEME OVERRIDES
   ========================================== */
.theme-light {
  /* All light theme overrides grouped here */
}

.theme-light .component-1 { }
.theme-light .component-2 { }
/* ... etc ... */

Why this structure:

  • Easy to find dark theme styles (main sections)
  • Easy to find light theme overrides (all in one place)
  • Prevents duplicate or conflicting styles

Common Mistakes to Avoid

❌ Forgetting Light Theme Overrides

/* Added new component styles */
.new-component {
  background: #1a2642;
  color: #e6edf3;
}

/* FORGOT to add .theme-light override */
/* Component is now unreadable in light theme! */

Always add both:

.new-component {
  background: #1a2642;
  color: #e6edf3;
}

.theme-light .new-component {
  background: #f0f4f8;
  color: #1e293b;
}

❌ Using Hardcoded Colors Instead of Variables

/* Don't repeat color values */
.component-a { color: #e6edf3; }
.component-b { color: #e6edf3; }
.component-c { color: #e6edf3; }

Use consistent palette (consider CSS variables in future):

/* Consistent color usage */
.component-a,
.component-b,
.component-c {
  color: #e6edf3;
}

❌ Low Contrast in Light Theme

.theme-light .text-secondary {
  color: #d1d5db;  /* Light gray on white = 1.8:1 contrast ❌ */
}

Check contrast ratios:

.theme-light .text-secondary {
  color: #6b7280;  /* Medium gray on white = 4.6:1 contrast ✅ */
}

❌ Scattering Light Theme Overrides Throughout File

/* Line 100 */
.component-a { }
.theme-light .component-a { }  /* Override here */

/* Line 300 */
.component-b { }
.theme-light .component-b { }  /* Override here */

/* Hard to maintain! */

Group all overrides together:

/* Lines 1-500: All dark theme styles */
.component-a { }
.component-b { }

/* Lines 500+: All light theme overrides grouped */
.theme-light .component-a { }
.theme-light .component-b { }

Testing Checklist

When adding new themed components:

  • Dark theme styles added in main CSS section
  • Light theme overrides added in .theme-light section
  • Text contrast ≥ 4.5:1 in dark theme (checked with DevTools)
  • Text contrast ≥ 4.5:1 in light theme (checked with DevTools)
  • Hover states work in both themes
  • Focus states visible in both themes
  • Animations smooth in both themes
  • Component tested on actual page in both themes
  • Screenshots taken for documentation (optional)

Future Enhancements

CSS Variables

Consider migrating to CSS variables for easier theme management:

:root {
  --bg-primary: #0d1117;
  --text-primary: #e6edf3;
  --border-color: rgba(58, 93, 176, 0.3);
}

.theme-light {
  --bg-primary: #ffffff;
  --text-primary: #1f2937;
  --border-color: #cbd5e1;
}

.component {
  background: var(--bg-primary);
  color: var(--text-primary);
  border: 1px solid var(--border-color);
}

Benefits:

  • Single source of truth for colors
  • Easier to add new themes
  • Fewer overrides needed

Not implemented yet - current system works well for two themes.