home / skills / etewiah / property_web_builder / theme-creation

theme-creation skill

/.claude/skills/theme-creation

This skill helps you create and customize themes for PropertyWebBuilder, handling registration, inheritance, palettes, and per-website styling.

npx playbooks add skill etewiah/property_web_builder --skill theme-creation

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
16.7 KB
---
name: theme-creation
description: Create new themes for PropertyWebBuilder. Use when creating custom themes, styling websites, or modifying theme templates. Handles theme registration, view templates, CSS, and asset configuration.
---

# Theme Creation for PropertyWebBuilder

## Theme System Overview

PropertyWebBuilder uses a multi-tenant theme system where each website can have its own theme. The system supports:
- **Theme inheritance** - Child themes extend parent themes
- **Color palettes** - Multiple pre-defined color schemes per theme
- **Page Part Library** - 20+ pre-built, customizable sections
- **CSS custom properties** - Native CSS variables for easy customization
- **Per-tenant customization** - Each website can override theme defaults
- **WCAG AA accessibility** - Built-in contrast checking utilities
- **Dark mode support** - Automatic or explicit dark mode colors

### Current Themes (January 2025)

| Theme | Parent | Status | Palettes | Description |
|-------|--------|--------|----------|-------------|
| `default` | None | Active | 6 | Base Tailwind/Flowbite theme |
| `brisbane` | default | Active | 6 | Luxury real estate (gold/navy) |
| `bologna` | default | Active | 4 | Traditional European style |
| `barcelona` | default | Disabled | 4 | Incomplete - needs work |
| `biarritz` | default | Disabled | 4 | Needs accessibility fixes |

### Key Components

| Component | Location | Purpose |
|-----------|----------|---------|
| Theme Registry | `app/themes/config.json` | Theme definitions |
| Theme Model | `app/models/pwb/theme.rb` | ActiveJSON model with inheritance |
| Palette Loader | `app/services/pwb/palette_loader.rb` | Load palettes from JSON |
| Palette Validator | `app/services/pwb/palette_validator.rb` | Validate against schema |
| Color Utils | `app/services/pwb/color_utils.rb` | WCAG contrast, shade generation |
| Palette Compiler | `app/services/pwb/palette_compiler.rb` | Compile CSS for production |
| Website Styleable | `app/models/concerns/pwb/website_styleable.rb` | Per-website styles |
| CSS Templates | `app/views/pwb/custom_css/_*.css.erb` | Dynamic CSS generation |

### Theme Resolution Flow

1. Request comes in with subdomain (tenant identification)
2. `ApplicationController#set_theme_path` determines theme from:
   - URL parameter `?theme=name` (if whitelisted)
   - Website's `theme_name` field
   - Fallback to "default"
3. Theme view paths are prepended (child first, then parent)
4. Views render from theme directory, falling back through inheritance chain

## Creating a New Theme

### Step 1: Register the Theme in config.json

Add to `app/themes/config.json`:

```json
{
  "name": "mytheme",
  "friendly_name": "My Custom Theme",
  "id": "mytheme",
  "version": "1.0.0",
  "enabled": true,
  "parent_theme": "default",
  "description": "A custom theme for my agency",
  "author": "Your Name",
  "tags": ["modern", "clean"],
  "supports": {
    "page_parts": [
      "heroes/hero_centered",
      "heroes/hero_split",
      "features/feature_grid_3col",
      "testimonials/testimonial_carousel",
      "cta/cta_banner"
    ],
    "layouts": ["default", "landing", "full_width"],
    "color_schemes": ["light", "dark"],
    "features": {
      "sticky_header": true,
      "back_to_top": true,
      "animations": true
    }
  },
  "style_variables": {
    "colors": {
      "primary_color": {
        "type": "color",
        "default": "#your-brand-color",
        "label": "Primary Color"
      },
      "secondary_color": {
        "type": "color",
        "default": "#your-secondary-color",
        "label": "Secondary Color"
      }
    },
    "typography": {
      "font_primary": {
        "type": "font_select",
        "default": "Open Sans",
        "label": "Primary Font",
        "options": ["Open Sans", "Roboto", "Montserrat"]
      }
    }
  }
}
```

### Step 2: Create Directory Structure

```bash
mkdir -p app/themes/mytheme/views/layouts/pwb
mkdir -p app/themes/mytheme/views/pwb/welcome
mkdir -p app/themes/mytheme/views/pwb/components
mkdir -p app/themes/mytheme/views/pwb/sections
mkdir -p app/themes/mytheme/views/pwb/pages
mkdir -p app/themes/mytheme/views/pwb/props
mkdir -p app/themes/mytheme/views/pwb/search
mkdir -p app/themes/mytheme/views/pwb/shared
mkdir -p app/themes/mytheme/palettes  # For color palette JSON files
mkdir -p app/themes/mytheme/page_parts  # For custom page part templates
```

### Step 3: Create Default Palette

Create `app/themes/mytheme/palettes/default.json`:

```json
{
  "id": "default",
  "name": "Default",
  "description": "Default color scheme for mytheme",
  "is_default": true,
  "preview_colors": ["#3498db", "#2c3e50", "#e74c3c"],
  "colors": {
    "primary_color": "#3498db",
    "secondary_color": "#2c3e50",
    "accent_color": "#e74c3c",
    "background_color": "#ffffff",
    "text_color": "#333333",
    "header_background_color": "#ffffff",
    "header_text_color": "#333333",
    "footer_background_color": "#2c3e50",
    "footer_text_color": "#ffffff",
    "light_color": "#f8f9fa",
    "link_color": "#3498db",
    "action_color": "#3498db"
  }
}
```

### Step 4: Copy and Customize Layout

Copy from parent theme:
```bash
cp app/themes/default/views/layouts/pwb/application.html.erb app/themes/mytheme/views/layouts/pwb/
```

Edit `app/themes/mytheme/views/layouts/pwb/application.html.erb`:

```erb
<!DOCTYPE html>
<html lang="<%= I18n.locale %>">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><%= yield(:page_title) || @current_website&.site_name %></title>
    <%= yield(:page_head) %>

    <%# Tailwind CSS for this theme %>
    <%= stylesheet_link_tag "tailwind-mytheme", "data-turbo-track": "reload" %>

    <%# Flowbite components %>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.3.0/flowbite.min.css" rel="stylesheet" />

    <%# Material Symbols for icons %>
    <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&display=swap" rel="stylesheet" />

    <%# Dynamic CSS variables %>
    <style>
      <%= custom_styles("mytheme") %>
    </style>

    <%= javascript_include_tag "pwb/application", async: false %>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.3.0/flowbite.min.js"></script>
    <%= csrf_meta_tags %>
  </head>
  <body class="tnt-body mytheme-theme <%= @current_website&.body_style %> bg-gray-50 text-gray-900">
    <div class="flex flex-col min-h-screen">
      <%= render partial: '/pwb/header', locals: { not_devise: true } %>
      <main class="flex-grow">
        <%= render 'devise/shared/messages' %>
        <%= yield %>
      </main>
      <%= render partial: '/pwb/footer', locals: {} %>
    </div>
    <%= yield(:page_script) %>
  </body>
</html>
```

### Step 5: Create Theme CSS Partial

Create `app/views/pwb/custom_css/_mytheme.css.erb`:

```erb
/* Theme: mytheme */
<%
  # Get palette colors merged with website overrides
  styles = @current_website&.style_variables || {}

  primary_color = styles["primary_color"] || "#3498db"
  secondary_color = styles["secondary_color"] || "#2c3e50"
  accent_color = styles["accent_color"] || "#e74c3c"
  background_color = styles["background_color"] || "#ffffff"
  text_color = styles["text_color"] || "#333333"
  header_bg = styles["header_background_color"] || "#ffffff"
  header_text = styles["header_text_color"] || "#333333"
  footer_bg = styles["footer_background_color"] || "#2c3e50"
  footer_text = styles["footer_text_color"] || "#ffffff"
  font_primary = styles["font_primary"] || "Open Sans"
  border_radius = styles["border_radius"] || "0.5rem"
%>

<%= render partial: 'pwb/custom_css/base_variables',
           locals: {
             primary_color: primary_color,
             secondary_color: secondary_color,
             accent_color: accent_color,
             background_color: background_color,
             text_color: text_color,
             font_primary: font_primary,
             border_radius: border_radius
           } %>

:root {
  --header-bg: <%= header_bg %>;
  --header-text: <%= header_text %>;
  --footer-bg: <%= footer_bg %>;
  --footer-text: <%= footer_text %>;
}

/* Theme-specific overrides */
.mytheme-theme header {
  background-color: var(--header-bg);
  color: var(--header-text);
}

.mytheme-theme footer {
  background-color: var(--footer-bg);
  color: var(--footer-text);
}

/* Custom raw CSS from admin */
<%= @current_website&.raw_css %>
```

### Step 6: Create Tailwind Input File

Create `app/assets/stylesheets/tailwind-mytheme.css`:

```css
@import "tailwindcss";

/* Font imports */
@font-face {
  font-family: 'Open Sans';
  font-weight: 400;
  src: url('https://cdn.jsdelivr.net/npm/@fontsource/[email protected]/files/open-sans-latin-400-normal.woff2');
}

/* Theme configuration */
@theme {
  --color-primary: var(--primary-color, #3498db);
  --color-secondary: var(--secondary-color, #2c3e50);
  --color-accent: var(--accent-color, #e74c3c);
  --font-family-sans: 'Open Sans', var(--font-primary, system-ui, sans-serif);
  --radius: var(--border-radius, 0.375rem);
}

/* PWB utility classes */
@layer utilities {
  .bg-pwb-primary { background-color: var(--pwb-primary); }
  .bg-pwb-secondary { background-color: var(--pwb-secondary); }
  .text-pwb-primary { color: var(--pwb-primary); }
  .text-pwb-secondary { color: var(--pwb-secondary); }
  .border-pwb-primary { border-color: var(--pwb-primary); }
}
```

### Step 7: Add Build Scripts

Add to `package.json`:

```json
{
  "scripts": {
    "tailwind:mytheme": "npx @tailwindcss/cli -i ./app/assets/stylesheets/tailwind-mytheme.css -o ./app/assets/builds/tailwind-mytheme.css --watch",
    "tailwind:mytheme:prod": "npx @tailwindcss/cli -i ./app/assets/stylesheets/tailwind-mytheme.css -o ./app/assets/builds/tailwind-mytheme.css --minify"
  }
}
```

### Step 8: Test the Theme

```ruby
# Via Rails console
theme = Pwb::Theme.find_by(name: 'mytheme')
theme.view_paths           # Verify path resolution
theme.palettes             # Check palettes loaded
theme.default_palette_id   # Verify default palette

# Update a website to use the theme
website = Pwb::Website.first
website.update(theme_name: 'mytheme')
```

```bash
# Build Tailwind CSS
npm run tailwind:mytheme:prod

# Via URL parameter (if enabled)
http://localhost:3000/?theme=mytheme
```

## Creating Color Palettes

### Palette File Structure

Palettes are stored in `app/themes/[theme]/palettes/*.json`:

```json
{
  "id": "my_palette",
  "name": "My Palette",
  "description": "A beautiful color palette",
  "is_default": false,
  "preview_colors": ["#primary", "#secondary", "#accent"],
  "colors": {
    "primary_color": "#e91b23",
    "secondary_color": "#2c3e50",
    "accent_color": "#3498db",
    "background_color": "#ffffff",
    "text_color": "#333333",
    "header_background_color": "#ffffff",
    "header_text_color": "#333333",
    "footer_background_color": "#2c3e50",
    "footer_text_color": "#ffffff",
    "light_color": "#f8f9fa",
    "link_color": "#e91b23",
    "action_color": "#e91b23"
  }
}
```

### Required Colors (9 mandatory)

| Key | Purpose |
|-----|---------|
| `primary_color` | Main brand color for CTAs and links |
| `secondary_color` | Supporting color for secondary elements |
| `accent_color` | Highlight color for special elements |
| `background_color` | Main page background |
| `text_color` | Primary text color |
| `header_background_color` | Header/nav background |
| `header_text_color` | Header/nav text |
| `footer_background_color` | Footer background |
| `footer_text_color` | Footer text |

### Dark Mode Support

For explicit dark mode colors, use the `modes` structure:

```json
{
  "id": "modern_dark",
  "name": "Modern with Dark Mode",
  "modes": {
    "light": {
      "primary_color": "#3498db",
      "background_color": "#ffffff",
      "text_color": "#333333"
    },
    "dark": {
      "primary_color": "#5dade2",
      "background_color": "#121212",
      "text_color": "#e8e8e8"
    }
  }
}
```

If you only provide `colors`, dark mode is auto-generated using `ColorUtils.generate_dark_mode_colors()`.

### Validation & Tools

```bash
# Validate all palettes
rake palettes:validate

# List available palettes for a theme
rake palettes:list[mytheme]

# Check WCAG contrast compliance
rake palettes:contrast[mytheme,my_palette]

# Generate shade scale for a color
rake palettes:shades[#3498db]
```

```ruby
# In Rails console
loader = Pwb::PaletteLoader.new
palettes = loader.load_theme_palettes("mytheme")
light = loader.get_light_colors("mytheme", "my_palette")
dark = loader.get_dark_colors("mytheme", "my_palette")

# Validate a palette
validator = Pwb::PaletteValidator.new
result = validator.validate(palette_hash)
result.valid?   # => true/false
result.errors   # => ["Missing required color: primary_color"]
```

## Search Page Layout Requirements

**IMPORTANT: Search pages MUST follow responsive layout requirements.**

### Desktop Layout (>=1024px)

Filters MUST be displayed BESIDE results (side-by-side), NOT above them:

```
+--------------------------------------------------+
|  +------------+  +----------------------------+  |
|  | Filters    |  | Search Results             |  |
|  | (1/4)      |  | (3/4 width)                |  |
|  +------------+  +----------------------------+  |
+--------------------------------------------------+
```

### Required HTML Structure

```erb
<div class="flex flex-wrap -mx-4">
  <!-- Sidebar Filters (1/4 on desktop, full on mobile) -->
  <div class="w-full lg:w-1/4 px-4 mb-6 lg:mb-0">
    <button class="lg:hidden w-full ..."
            data-controller="search-form"
            data-action="click->search-form#toggleFilters">
      Filter Properties
    </button>
    <div id="sidebar-filters" class="hidden lg:block">
      <%= render 'pwb/searches/search_form_for_sale' %>
    </div>
  </div>

  <!-- Search Results (3/4 on desktop, full on mobile) -->
  <div class="w-full lg:w-3/4 px-4">
    <div id="inmo-search-results">
      <%= render 'search_results' %>
    </div>
  </div>
</div>
```

## PWB CSS Class Naming

Use semantic PWB classes for consistency:

```css
/* Colors */
.bg-pwb-primary { background-color: var(--pwb-primary); }
.bg-pwb-secondary { background-color: var(--pwb-secondary); }
.text-pwb-primary { color: var(--pwb-primary); }

/* Buttons */
.pwb-btn--primary { background-color: var(--pwb-primary); }
.pwb-btn--secondary { background-color: var(--pwb-secondary); }
.pwb-btn--outline { border: 2px solid var(--pwb-primary); }

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

/* Grid */
.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); }
```

## WCAG Accessibility Requirements

### Contrast Ratios (WCAG 2.1 AA)

| Text Type | Minimum Ratio |
|-----------|---------------|
| Normal text (<18px) | 4.5:1 |
| Large text (>=18px bold or >=24px) | 3:1 |
| UI components & graphics | 3:1 |

### Check Contrast in Ruby

```ruby
# Check if colors meet WCAG AA
Pwb::ColorUtils.wcag_aa_compliant?('#ffffff', '#333333')
# => true (14.0:1 ratio)

# Get exact contrast ratio
Pwb::ColorUtils.contrast_ratio('#ffffff', '#9ca3af')
# => 2.9 (fails AA - needs 4.5:1)

# Get suggested text color for a background
Pwb::ColorUtils.suggest_text_color('#1a2744')
# => '#ffffff' (white for dark backgrounds)
```

## Theme Inheritance

### How It Works

Child themes inherit from parent themes:

```ruby
theme = Pwb::Theme.find_by(name: 'brisbane')
theme.parent_theme        # => "default"
theme.parent              # => <Pwb::Theme name="default">
theme.inheritance_chain   # => [brisbane, default]
theme.view_paths          # => [brisbane/views, default/views, app/views]
```

### View Resolution Order

1. Check child theme: `app/themes/brisbane/views/`
2. Check parent theme: `app/themes/default/views/`
3. Check application: `app/views/`

## Troubleshooting

### Theme Not Loading

1. Check entry exists in `app/themes/config.json`
2. Verify `"enabled": true` is set
3. Verify JSON syntax is valid
4. Restart Rails server after config changes
5. Check: `Pwb::Theme.find_by(name: 'mytheme')`

### Styles Not Applying

1. Verify CSS partial exists: `app/views/pwb/custom_css/_mytheme.css.erb`
2. Verify Tailwind CSS is built: `app/assets/builds/tailwind-mytheme.css`
3. Check body class matches theme name (`.mytheme-theme`)
4. Clear Rails cache: `Rails.cache.clear`

### Palette Not Found

1. Check file exists: `app/themes/mytheme/palettes/default.json`
2. Validate JSON syntax
3. Run: `rake palettes:validate`
4. Check: `Pwb::PaletteLoader.new.load_theme_palettes('mytheme')`

## Documentation Reference

- `docs/theming/README.md` - Documentation index
- `docs/theming/THEME_AND_COLOR_SYSTEM.md` - Complete architecture
- `docs/theming/color-palettes/COLOR_PALETTES_ARCHITECTURE.md` - Palette system
- `docs/theming/THEME_CREATION_CHECKLIST.md` - Step-by-step checklist
- `app/themes/shared/color_schema.json` - Palette JSON schema

Overview

This skill creates and manages custom themes for PropertyWebBuilder, handling registration, view templates, CSS, palettes, and asset configuration. It simplifies theme inheritance, per-tenant overrides, WCAG contrast checks, and dark mode support so you can ship consistent branded sites quickly.

How this skill works

The skill registers a theme in app/themes/config.json and provides a directory structure for views, palettes, and page parts. It compiles palettes and CSS (Tailwind input + dynamic ERB partials), resolves theme view paths with inheritance, and exposes palette loading/validation and contrast tools to ensure accessible color schemes.

When to use it

  • You need a new look and feel for a tenant or agency site
  • Building custom page parts, layouts, or branded landing pages
  • Creating or validating color palettes including dark mode variants
  • Overriding parent theme views while preserving fallback behavior
  • Preparing production-ready Tailwind/CSS builds for a theme

Best practices

  • Register your theme in app/themes/config.json with supported page_parts, layouts, and style_variables
  • Place view overrides only for components you change; rely on parent fallback to minimize duplication
  • Define at least the nine required colors for palettes to ensure layout and accessibility
  • Use PaletteValidator and rake palettes:validate during development CI
  • Provide explicit dark mode modes or rely on ColorUtils to generate dark variants and then review contrast

Example use cases

  • Create a branded agency theme that inherits from default and adds a luxury palette
  • Add a landing layout and new hero page parts for a campaign, then register them in supports.page_parts
  • Author multiple palette JSON files and run rake palettes:contrast to meet WCAG AA before release
  • Copy default layout into a new theme and tweak header/footer variables via custom_css partials
  • Add a tailwind-mytheme build script and run tailwind:mytheme:prod during deployment

FAQ

How do I enable dark mode for my theme?

Provide a modes object in a palette JSON (light/dark). If you only provide colors, ColorUtils will auto-generate a dark mode, but you should validate contrast and tweak as needed.

Where do per-website overrides live?

Per-website overrides come from the website.style_variables and raw_css fields; dynamic CSS partials (app/views/pwb/custom_css/_yourtheme.css.erb) merge those at runtime.