Skip to content

Latest commit

 

History

History
915 lines (717 loc) · 23.6 KB

File metadata and controls

915 lines (717 loc) · 23.6 KB

Theming System Documentation

⚠️ CSS Framework Migration: Bootstrap CSS is deprecated as of December 2024. All new themes should use Tailwind CSS. Legacy Bootstrap styles remain for backward compatibility but will not receive updates. See vendor/assets/stylesheets/bootstrap/DEPRECATED.md for migration guidance.

Table of Contents

  1. Overview
  2. Architecture
  3. Theme Configuration
  4. Page Part Library
  5. CSS Custom Properties
  6. Theme Settings Schema
  7. Custom Liquid Tags
  8. Theme Inheritance
  9. Per-Tenant Customization
  10. Creating a New Theme
  11. API Reference

Overview

The PropertyWebBuilder theming system provides a flexible, extensible architecture for creating and customizing website themes. The system supports:

  • Theme inheritance: Child themes can extend parent themes
  • Page part library: 20+ pre-built, customizable page sections
  • CSS custom properties: Native CSS variables for easy customization
  • Per-tenant customization: Each website can customize theme variables
  • Custom Liquid tags: Dynamic content rendering within templates

Key Components

Component Location Purpose
Theme Model app/models/pwb/theme.rb Theme metadata, inheritance, capabilities
Page Part Library app/lib/pwb/page_part_library.rb Registry of available page parts
Theme Settings Schema app/lib/pwb/theme_settings_schema.rb UI schema for theme customization
CSS Variables app/views/pwb/custom_css/_base_variables.css.erb Core CSS custom properties
Liquid Tags app/lib/pwb/liquid_tags/ Custom Liquid template tags
Theme Config app/themes/config.json Theme definitions and metadata

Architecture

File Structure

app/
├── themes/
│   ├── config.json              # Theme definitions
│   ├── default/
│   │   └── views/pwb/           # Default theme views
│   └── brisbane/
│       └── views/pwb/           # Brisbane theme overrides
├── views/pwb/
│   ├── page_parts/              # Page part templates
│   │   ├── heroes/
│   │   ├── features/
│   │   ├── testimonials/
│   │   ├── cta/
│   │   ├── stats/
│   │   ├── teams/
│   │   ├── galleries/
│   │   ├── faqs/
│   │   └── pricing/
│   └── custom_css/
│       ├── _base_variables.css.erb
│       └── _component_styles.css.erb
├── lib/pwb/
│   ├── page_part_library.rb
│   ├── theme_settings_schema.rb
│   └── liquid_tags/
│       ├── property_card_tag.rb
│       ├── featured_properties_tag.rb
│       ├── contact_form_tag.rb
│       └── page_part_tag.rb
└── models/pwb/
    └── theme.rb

Data Flow

┌─────────────────┐     ┌──────────────────┐     ┌────────────────┐
│  Theme Config   │────▶│   Theme Model    │────▶│  View Paths    │
│  (config.json)  │     │   (theme.rb)     │     │  (prepended)   │
└─────────────────┘     └──────────────────┘     └────────────────┘
                                │
                                ▼
                        ┌──────────────────┐
                        │  Website Model   │
                        │ (style_variables)│
                        └──────────────────┘
                                │
                                ▼
┌─────────────────┐     ┌──────────────────┐     ┌────────────────┐
│ Page Part       │────▶│  Liquid Template │────▶│ Rendered HTML  │
│ Library         │     │  (with tags)     │     │                │
└─────────────────┘     └──────────────────┘     └────────────────┘

Theme Configuration

config.json Structure

Themes are defined in app/themes/config.json:

{
  "name": "default",
  "friendly_name": "Default Theme",
  "id": "default",
  "version": "2.0.0",
  "description": "A clean, modern theme suitable for any real estate website",
  "author": "PropertyWebBuilder",
  "tags": ["modern", "minimal", "responsive"],
  "parent_theme": null,
  "screenshots": ["url/to/screenshot.png"],
  "supports": {
    "page_parts": ["heroes/hero_centered", "features/feature_grid_3col"],
    "layouts": ["default", "landing", "full_width", "sidebar"],
    "color_schemes": ["light", "dark"],
    "features": {
      "sticky_header": true,
      "back_to_top": true,
      "preloader": false,
      "animations": true
    }
  },
  "style_variables": {
    "colors": {
      "primary_color": {
        "type": "color",
        "default": "#e91b23",
        "label": "Primary Color",
        "description": "Main brand color"
      }
    },
    "typography": {
      "font_primary": {
        "type": "font_select",
        "default": "Open Sans",
        "label": "Primary Font",
        "options": ["Open Sans", "Roboto", "Lato"]
      }
    }
  },
  "page_parts_config": {
    "heroes": {
      "default_variant": "hero_centered",
      "available_variants": ["hero_centered", "hero_split", "hero_search"]
    }
  }
}

Configuration Fields

Field Type Description
name String Internal theme identifier
friendly_name String Display name for UI
version String Semantic version number
parent_theme String/null Parent theme name for inheritance
supports.page_parts Array List of supported page part keys
supports.layouts Array Available layout options
supports.color_schemes Array Color scheme variants
supports.features Object Feature flags
style_variables Object Customizable style variables by category
page_parts_config Object Category-specific page part configuration

Page Part Library

The Page Part Library (Pwb::PagePartLibrary) provides a registry of all available page part templates.

Categories

Category Label Description
heroes Hero Sections Large banner sections for page tops
features Features Sections showcasing services/benefits
testimonials Testimonials Customer reviews and testimonials
cta Call to Action Sections encouraging user action
stats Statistics Number counters and statistics
teams Team Team member profiles
galleries Galleries Image galleries and portfolios
pricing Pricing Pricing tables and comparisons
faqs FAQs Frequently asked questions
content Content General content sections
contact Contact Contact forms and information

Available Page Parts

Heroes

  • heroes/hero_centered - Full-width hero with centered content
  • heroes/hero_split - Two-column hero with image
  • heroes/hero_search - Hero with property search form

Features

  • features/feature_grid_3col - Three feature cards in grid
  • features/feature_cards_icons - Four icon cards with colors

Testimonials

  • testimonials/testimonial_carousel - Sliding carousel
  • testimonials/testimonial_grid - Grid of testimonial cards

Call to Action

  • cta/cta_banner - Full-width CTA banner
  • cta/cta_split_image - Split CTA with image

Statistics

  • stats/stats_counter - Animated number counters

Teams

  • teams/team_grid - Team member grid with social links

Galleries

  • galleries/image_gallery - Grid gallery with lightbox

FAQs

  • faqs/faq_accordion - Expandable FAQ section

Pricing

  • pricing/pricing_table - Three-column pricing comparison

Usage

# Get all page part keys
Pwb::PagePartLibrary.all_keys
# => ["heroes/hero_centered", "heroes/hero_split", ...]

# Get page parts by category
Pwb::PagePartLibrary.for_category(:heroes)
# => { "heroes/hero_centered" => { category: :heroes, ... } }

# Get definition for specific page part
Pwb::PagePartLibrary.definition("heroes/hero_centered")
# => { category: :heroes, label: "Centered Hero", fields: [...] }

# Get template path
Pwb::PagePartLibrary.template_path("heroes/hero_centered")
# => #<Pathname:app/views/pwb/page_parts/heroes/hero_centered.liquid>

# Get JSON schema for API
Pwb::PagePartLibrary.to_json_schema

CSS Custom Properties

Base Variables

The CSS custom properties system uses native CSS variables defined in _base_variables.css.erb:

:root {
  /* Color System */
  --pwb-primary: <%= primary_color %>;
  --pwb-primary-light: color-mix(in srgb, <%= primary_color %> 70%, white);
  --pwb-primary-dark: color-mix(in srgb, <%= primary_color %> 70%, black);
  --pwb-secondary: <%= secondary_color %>;
  --pwb-accent: <%= accent_color %>;

  /* Typography */
  --pwb-font-primary: <%= font_primary %>;
  --pwb-font-heading: <%= font_heading %>;
  --pwb-font-size-base: <%= font_size_base %>;

  /* Layout */
  --pwb-container-width: <%= container_width %>;
  --pwb-border-radius: <%= border_radius %>;

  /* Spacing Scale */
  --pwb-space-xs: 0.25rem;
  --pwb-space-sm: 0.5rem;
  --pwb-space-md: 1rem;
  --pwb-space-lg: 1.5rem;
  --pwb-space-xl: 2rem;
}

Component Styles

Component styles in _component_styles.css.erb use these variables:

/* Buttons */
.pwb-btn--primary {
  background-color: var(--pwb-primary);
  border-radius: var(--pwb-border-radius);
}

/* Cards */
.pwb-card {
  border-radius: var(--pwb-border-radius);
  box-shadow: var(--pwb-shadow-md);
}

/* Hero Sections */
.pwb-hero {
  font-family: var(--pwb-font-heading);
}

Grid System

.pwb-grid--2col { grid-template-columns: repeat(2, 1fr); }
.pwb-grid--3col { grid-template-columns: repeat(3, 1fr); }
.pwb-grid--4col { grid-template-columns: repeat(4, 1fr); }

@media (max-width: 768px) {
  .pwb-grid--2col,
  .pwb-grid--3col,
  .pwb-grid--4col {
    grid-template-columns: 1fr;
  }
}

Theme Settings Schema

The Pwb::ThemeSettingsSchema defines the structure for theme customization UIs.

Schema Structure

SCHEMA = {
  colors: {
    label: "Colors",
    description: "Customize your website's color palette",
    icon: "palette",
    order: 1,
    fields: {
      primary_color: {
        type: :color,
        default: "#e91b23",
        label: "Primary Color",
        description: "Main brand color",
        css_variable: "--pwb-primary"
      },
      secondary_color: {
        type: :color,
        default: "#2c3e50",
        label: "Secondary Color"
      }
    }
  },
  typography: {
    label: "Typography",
    fields: {
      font_primary: {
        type: :font_select,
        default: "Open Sans",
        label: "Primary Font",
        options: ["Open Sans", "Roboto", "Lato", ...]
      }
    }
  }
}

Field Types

Type Description Properties
:color Color picker default, css_variable
:font_select Font dropdown options, default
:select Generic dropdown options, default
:number Numeric input min, max, step, unit
:toggle Boolean switch default
:text Text input default, placeholder

Usage

# Get full schema
Pwb::ThemeSettingsSchema::SCHEMA

# Get schema for specific section
Pwb::ThemeSettingsSchema::SCHEMA[:colors]

# Access field metadata
field = Pwb::ThemeSettingsSchema::SCHEMA[:colors][:fields][:primary_color]
field[:type]     # => :color
field[:default]  # => "#e91b23"

Custom Liquid Tags

Custom Liquid tags extend template functionality with PropertyWebBuilder-specific features.

property_card

Renders a property card for a specific property.

{% property_card 123 %}
{% property_card property_id %}
{% property_card 123, style: "compact" %}

Options:

  • style: Card style variant ("default", "compact")

featured_properties

Renders a grid of featured properties.

{% featured_properties %}
{% featured_properties limit: 6 %}
{% featured_properties limit: 4, type: "sale" %}
{% featured_properties limit: 3, style: "compact", columns: 3 %}
{% featured_properties highlighted: "true" %}

Options:

  • limit: Number of properties (default: 6)
  • type: Filter by type ("sale", "rent", "all")
  • style: Grid style ("default", "compact", "card", "grid")
  • columns: Number of columns (default: 3)
  • highlighted: Show only highlighted properties
  • show_price: Show/hide price (default: true)
  • show_location: Show/hide location (default: true)

contact_form

Renders a contact form.

{% contact_form %}
{% contact_form style: "compact" %}
{% contact_form style: "inline", property_id: 123 %}

Options:

  • style: Form style ("default", "compact", "inline", "sidebar")
  • property_id: Associate with a property
  • show_phone: Show phone field (default: true)
  • show_message: Show message field (default: true)
  • button_text: Custom button text
  • success_message: Custom success message

page_part

Renders another page part inline.

{% page_part "heroes/hero_centered" %}
{% page_part "cta/cta_banner", style: "primary" %}

Behavior:

  1. First tries to find a saved PagePart in the database
  2. Falls back to rendering directly from template file
  3. Supports nested page part rendering

Theme Inheritance

Themes can inherit from parent themes, allowing customization without duplication.

How Inheritance Works

{
  "name": "brisbane",
  "parent_theme": "default",
  ...
}

When Brisbane theme is active:

  1. Views are searched in Brisbane theme first
  2. If not found, falls back to default theme
  3. If not found there, uses application defaults

Theme Model Methods

theme = Pwb::Theme.find("brisbane")

# Check inheritance
theme.has_parent?
# => true

theme.parent
# => #<Pwb::Theme name="default">

# Get full inheritance chain
theme.inheritance_chain
# => [#<Pwb::Theme name="brisbane">, #<Pwb::Theme name="default">]

# Get view paths (child theme first)
theme.view_paths
# => ["app/themes/brisbane/views", "app/themes/default/views"]

Page Part Inheritance

Child themes can:

  • Override specific page part templates
  • Add new page part variants
  • Use parent theme's page parts as-is
# Check if theme has custom template
theme.has_custom_template?("heroes/hero_centered")
# => false (uses parent's template)

# Get available page parts (including inherited)
theme.available_page_parts
# => ["heroes/hero_centered", "heroes/hero_split", ...]

Per-Tenant Customization

Each website can customize theme variables without affecting other tenants.

Website Style Variables

website = Pwb::Website.find(1)

# Get current style variables
website.style_variables
# => { "primary_color" => "#ff0000", "font_primary" => "Roboto" }

# Update style variables
website.update(style_variables: {
  "primary_color" => "#00ff00",
  "secondary_color" => "#333333"
})

Merging with Theme Defaults

# Get theme defaults
theme = Pwb::Theme.find(website.theme_name)
defaults = theme.style_variable_defaults
# => { "primary_color" => "#e91b23", ... }

# Merge with website overrides
effective_styles = defaults.merge(website.style_variables || {})

In Views

<%# app/views/pwb/custom_css/custom.css.erb %>
<%
  theme = Pwb::Theme.find(current_website.theme_name)
  defaults = theme.style_variable_defaults
  styles = defaults.merge(current_website.style_variables || {})

  primary_color = styles["primary_color"]
  font_primary = styles["font_primary"]
%>

<%= render "pwb/custom_css/base_variables",
           primary_color: primary_color,
           font_primary: font_primary %>

Creating a New Theme

Step 1: Add Theme Configuration

Add to app/themes/config.json:

{
  "name": "my_theme",
  "friendly_name": "My Custom Theme",
  "id": "my_theme",
  "version": "1.0.0",
  "parent_theme": "default",
  "description": "A custom theme for my agency",
  "supports": {
    "page_parts": ["heroes/hero_centered", "features/feature_grid_3col"],
    "layouts": ["default", "landing"],
    "color_schemes": ["light"]
  },
  "style_variables": {
    "colors": {
      "primary_color": {
        "type": "color",
        "default": "#your-brand-color"
      }
    }
  }
}

Step 2: Create Theme Directory

mkdir -p app/themes/my_theme/views/pwb

Step 3: Override Views (Optional)

Copy and modify views from parent theme:

cp -r app/themes/default/views/pwb/layouts app/themes/my_theme/views/pwb/

Step 4: Create Custom Page Part Variants (Optional)

mkdir -p app/themes/my_theme/views/pwb/page_parts/heroes

Create hero_custom.liquid:

<section class="my-theme-hero">
  <div class="container">
    <h1>{{ page_part.title.content }}</h1>
    <p>{{ page_part.subtitle.content }}</p>
  </div>
</section>

Step 5: Add Theme CSS

Create app/views/pwb/custom_css/_my_theme.css.erb:

/* My Theme Custom Styles */
.my-theme-hero {
  background: linear-gradient(135deg, var(--pwb-primary), var(--pwb-secondary));
  padding: var(--pwb-space-xl) 0;
}

Step 6: Test the Theme

# In Rails console
website = Pwb::Website.first
website.update(theme_name: "my_theme")

# Verify theme loads
theme = Pwb::Theme.find("my_theme")
theme.view_paths
theme.available_page_parts

API Reference

Theme Model

class Pwb::Theme
  # Class Methods
  Theme.all           # => Array of all themes
  Theme.find(name)    # => Theme instance or nil
  Theme.find!(name)   # => Theme instance or raises
  Theme.default       # => Default theme

  # Instance Methods
  theme.name              # => "brisbane"
  theme.friendly_name     # => "Brisbane Luxury Theme"
  theme.version           # => "2.0.0"
  theme.description       # => "A luxurious theme..."
  theme.author            # => "PropertyWebBuilder"
  theme.tags              # => ["luxury", "elegant"]
  theme.screenshots       # => ["url1", "url2"]

  # Inheritance
  theme.parent_theme      # => "default" or nil
  theme.parent            # => Theme instance or nil
  theme.has_parent?       # => true/false
  theme.inheritance_chain # => [child, parent, grandparent, ...]

  # Paths
  theme.root_path         # => Pathname to theme directory
  theme.view_paths        # => Array of view paths

  # Capabilities
  theme.supported_page_parts  # => ["heroes/hero_centered", ...]
  theme.supported_layouts     # => ["default", "landing", ...]
  theme.supported_color_schemes # => ["light", "dark"]
  theme.supported_features    # => { sticky_header: true, ... }

  # Page Parts
  theme.has_custom_template?(key) # => true/false
  theme.available_page_parts      # => All available parts
  theme.page_part_variants(category) # => Variants for category

  # Style Variables
  theme.style_variable_schema   # => Full schema from config
  theme.style_variable_defaults # => Default values hash

  # Serialization
  theme.as_api_json # => Hash for API responses
end

PagePartLibrary

module Pwb::PagePartLibrary
  # Categories
  CATEGORIES  # => Hash of category definitions

  # Definitions
  DEFINITIONS # => Hash of all page part definitions

  # Query Methods
  all_keys           # => Array of all keys
  by_category        # => Hash grouped by category
  for_category(cat)  # => Parts for specific category
  definition(key)    # => Definition hash for key
  exists?(key)       # => true/false

  # Templates
  template_exists?(key) # => true/false
  template_path(key)    # => Pathname or nil

  # Categories
  categories         # => All category definitions
  category_info(cat) # => Single category info

  # Filtering
  modern_parts      # => Non-legacy parts
  legacy_parts      # => Legacy parts only

  # API
  to_json_schema    # => Full schema for API
end

ThemeSettingsSchema

module Pwb::ThemeSettingsSchema
  SCHEMA # => Full schema hash

  # Structure:
  # {
  #   section_key: {
  #     label: "Section Name",
  #     description: "...",
  #     icon: "icon-name",
  #     order: 1,
  #     fields: {
  #       field_key: {
  #         type: :color|:font_select|:select|:number|:toggle|:text,
  #         default: "value",
  #         label: "Field Label",
  #         description: "...",
  #         options: [...],  # for select types
  #         min: 0, max: 100, step: 1, unit: "px"  # for number
  #       }
  #     }
  #   }
  # }
end

Best Practices

1. Use Semantic CSS Classes

<!-- Good -->
<div class="hero-section">
  <h1 class="hero-title">{{ title }}</h1>
</div>

<!-- Avoid -->
<div class="bg-gray-900 h-[600px] flex items-center">
  <h1 class="text-4xl font-bold text-white">{{ title }}</h1>
</div>

2. Leverage CSS Variables

/* Good - Uses theme variables */
.hero-title {
  color: var(--pwb-primary);
  font-family: var(--pwb-font-heading);
}

/* Avoid - Hardcoded values */
.hero-title {
  color: #e91b23;
  font-family: "Montserrat", sans-serif;
}

3. Design for Inheritance

When creating themes, consider:

  • Only override what needs to change
  • Use parent theme's components where possible
  • Keep customizations minimal and focused

4. Test Across Tenants

# Ensure tenant isolation
Pwb::Website.find_each do |website|
  Pwb::Current.website = website
  theme = Pwb::Theme.find(website.theme_name)

  # Verify theme loads correctly
  assert theme.present?
  assert theme.view_paths.all? { |p| File.directory?(p) }
end

5. Document Custom Page Parts

When adding custom page parts, update the library:

# In app/lib/pwb/page_part_library.rb
DEFINITIONS = {
  'my_theme/custom_hero' => {
    category: :heroes,
    label: 'Custom Hero',
    description: 'Theme-specific hero variant',
    fields: %w[title subtitle background_image]
  }
}

Troubleshooting

Theme Not Loading

  1. Check theme exists in config.json
  2. Verify website's theme_name matches config
  3. Check view paths are correct:
    theme = Pwb::Theme.find(current_website.theme_name)
    puts theme.view_paths

Styles Not Applying

  1. Clear Rails cache: Rails.cache.clear
  2. Check CSS variables are defined in _base_variables.css.erb
  3. Verify website's style_variables JSON is valid

Page Part Not Rendering

  1. Check template exists: Pwb::PagePartLibrary.template_exists?(key)
  2. Verify Liquid syntax in template
  3. Check block_contents has data for current locale

Liquid Tag Errors

  1. Ensure tags are loaded: check config/initializers/liquid.rb
  2. Verify tag syntax matches documentation
  3. Check Rails logs for Liquid parsing errors

Migration from Legacy System

If migrating from the legacy theming system:

1. Update Theme Config

Convert old YAML configs to new JSON format in config.json.

2. Migrate Style Variables

# Old format (in website model)
website.custom_css_styles # => "primary_color: #ff0000\n..."

# New format
website.style_variables # => { "primary_color" => "#ff0000" }

3. Update Page Parts

Legacy page parts are supported but marked:

Pwb::PagePartLibrary.legacy_parts
# => { "our_agency" => { legacy: true, ... } }

Consider migrating to modern equivalents for better support.