🎯

theme-creation

🎯Skill

from etewiah/property_web_builder

VibeIndex|
What it does

theme-creation skill from etewiah/property_web_builder

theme-creation

Installation

Install skill:
npx skills add https://github.com/etewiah/property_web_builder --skill theme-creation
1
AddedJan 29, 2026

Skill Details

SKILL.md

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.

Overview

# 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"

  1. Theme view paths are prepended (child first, then parent)
  2. 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

<%= yield(:page_title) || @current_website&.site_name %>

<%= yield(:page_head) %>

<%# Tailwind CSS for this theme %>

<%= stylesheet_link_tag "tailwind-mytheme", "data-turbo-track": "reload" %>

<%# Flowbite components %>

<%# Material Symbols for icons %>

<%# Dynamic CSS variables %>

<%= javascript_include_tag "pwb/application", async: false %>

<%= csrf_meta_tags %>

<%= render partial: '/pwb/header', locals: { not_devise: true } %>

<%= render 'devise/shared/messages' %>

<%= yield %>

<%= render partial: '/pwb/footer', locals: {} %>

<%= yield(:page_script) %>

```

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/open-sans@5.2.5/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

<%= render 'search_results' %>

```

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 # =>

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