Product Comparison Table — Architecture & Usage Guide

Overview

Www::ProductCompareTableComponent renders a responsive, scrollable product comparison table
used on all product controls / thermostat pages. Each product appears as a column; feature
rows run horizontally with a sticky label column on the left.

The component is driven entirely by a static SKU_FEATURES hash — no additional database
queries are needed beyond the standard product presenter load. This keeps it fast and easy
to maintain.

Used on:

  • floor-heating/thermostats
  • radiant-heat-panels/controls
  • snow-melting/controls
  • towel-warmer/controls
  • pipe-freeze-protection/controls
  • roof-and-gutter-deicing/controls

Files

File Purpose
app/components/www/product_compare_table_component.rb Ruby component: data loading, feature lookup, helpers
app/components/www/product_compare_table_component.html.erb ERB template: 5-sub-row header + feature rows
client/stylesheets/www/03-components/product_compare_table_component.scss Scoped SCSS: sticky column, cell alignment, responsive breakpoints

Constructor Parameters

Www::ProductCompareTableComponent.new(
  skus:               Array,   # required — ordered list of product SKUs
  features:           Array,   # optional — feature keys to show; auto-detected if nil
  section_title:      String,  # optional — H2 above the table
  section_description: String, # optional — paragraph below the title
  sort_by_price:      Boolean  # default: true — set false to preserve SKU array order
)

sort_by_price

By default products are sorted ascending by effective_price. Pass sort_by_price: false
when the SKU array encodes a meaningful order (e.g. thermostat tier: WiFi → Programmable →
Non-programmable → Power module). All other callers retain the default true.


Template Structure

The header is split into 4 dedicated sub-rows so that varying title / tagline lengths
never push sibling product columns out of vertical alignment:

┌────────────────────────────────────────────────────────┐
│ IMAGE ROW      │ [img] │ [img] │ [img] │ [img]         │  align-items: end
│ TITLE ROW      │ Title │ Title │ Title │ Title         │  align-items: stretch
│ TAGLINE ROW*   │  tag  │  tag  │  tag  │  tag          │  align-items: stretch
│ SKU+PRICE ROW  │ [sku] │ [sku] │ [sku] │ [sku]         │  align-items: stretch
│  "Features" ↙  │ $249  │ $259  │ $159  │ $149          │
├────────────────┼───────┼───────┼───────┼───────────────┤
│ WiFi-Enabled   │  ✓    │  ✗    │  ✗    │  ✗            │  feature rows
│ Programmable   │  ✓    │  ✓    │  ✗    │  —            │
│ …              │       │       │       │               │
├────────────────┼───────┼───────┼───────┼───────────────┤
│                │[View] │[View] │[View] │ [View]        │  CTA row
└────────────────┴───────┴───────┴───────┴───────────────┘

* Tagline row is omitted when no product has a short_description.


Feature Value Types

Each entry in SKU_FEATURES[sku][feature_key] can be:

Value Renders as
true Green check-circle icon (with tooltip popover if tip defined)
false Muted × icon
String Small text badge (e.g. "Up to 10 schedules", "LED touch")
:aerial / :slab Yellow "Pair with…" badge linking to the sensors section
nil / absent "N/A" in muted text

Sensor features (moisture_sensor, air_temp_sensor, slab_temp_sensor) additionally
render true as a green "Included" pill badge instead of a plain checkmark.


Adding a New SKU

  1. Add the SKU to SKU_FEATURES in product_compare_table_component.rb:
'MY-SKU' => {
  'wifi'                  => true,
  'remote_support'        => false,
  'voice_control'         => true,
  'programmable'          => true,
  'schedules'             => 'Up to 6 per day',
  'floor_sensor_included' => true,
  'display'               => '3.5" touchscreen',
  'max_load'              => '15A @ 120/240V'
},
  1. Include the SKU in the page's SKU array:
tstat_skus = %w[UWG5-4999-WY MY-SKU UTN5-4999]
  1. Pass the array to the component. If the SKU has no entry in SKU_FEATURES, all its
    feature cells fall back to false (× icon).

Adding a New Feature Row

  1. Add a tooltip in FEATURE_TOOLTIPS:
'my_feature' => 'Plain-English explanation shown on hover/focus.'
  1. Add a display name in feature_display_name:
when 'my_feature' then 'My Feature Label'
  1. Add the value for every relevant SKU in SKU_FEATURES.

  2. Include 'my_feature' in the features: array on the calling page.


Alignment Architecture

The SCSS (.product-compare-table) defines two complementary rules:

.feature-cell {
  display: flex;
  align-items: center;
  justify-content: center;   // product value cells: horizontally centred
  text-align: center;
}

.sticky-column {
  justify-content: flex-start; // overrides feature-cell — label column always left
  text-align: left;
}

Without the .sticky-column override, short single-line labels (e.g. "WiFi-Enabled")
would appear visually centred while long wrapping labels (e.g. "Remote Troubleshooting /
Updates") appeared left — an inconsistency caused by flex centering a short inline text node.


Mobile / Responsive Behaviour

  • The table-scroll-container uses overflow-x: auto — the table scrolls horizontally
    on narrow viewports rather than collapsing columns.
  • A dynamic min-width inline style (200 + products.size × 160 px) is applied to the
    inner wrapper to prevent columns from becoming unreadably narrow before the scroll kicks in.
  • The sticky label column (position: sticky; left: 0) remains visible as the user scrolls
    right, keeping feature labels always in view.
  • min-width of the sticky column scales down at tablet / compact-laptop breakpoints:
    240px200px180px160px.

Floor-Heating Thermostats Page

URL

/floor-heating/thermostats

Section Order

# Section ID Component / Content
1 hero Www::FullWidthLandingPageHeaderComponent
2 benefits Www::BenefitsListComponent — 4 universal thermostat guarantees
3 comparison Www::ProductCompareTableComponent — thermostats + nJoin module
4 accessories Www::ProductCardsComponent — sensor + 3 rough-in kits
5 guide "Which thermostat is right for you?" — 3 editorial cards
6 shop-by-system Internal cross-reference links (floor type / room / system)
7 showcases Www::ShowcaseGridComponent — conditional on CMS content
8 videos Www::VideoSectionComponent — conditional on CMS content
9 documents Www::CardGridComponent — conditional on CMS content
10 posts Www::CardGridComponent — conditional on CMS content
11 faq Www::FaqListComponent — conditional on CMS content
12 contact lazy_lead_form_modal

SKU Configuration

# US market
tstat_skus     = %w[UWG5-4999-WY UDG4-4999 UTN5-4999 USG5-4000]
accessory_skus = %w[FH-BACKUP-SENSOR FHE-ROUGH-IN-KIT-S1 FHE-ROUGH-IN-KIT-S2 FHE-ROUGH-IN-KIT-S3]

# Canada
tstat_skus     = %w[UWG5-4999-WY UDG4-4999-WY UTN5-4999 USG-4000]
accessory_skus = %w[FH-BACKUP-SENSOR FHE-ROUGH-IN-KIT-S1 FHE-ROUGH-IN-KIT-S2 FHE-ROUGH-IN-KIT-S3]

The nJoin power module (USG5-4000 / USG-4000) appears last in the comparison table
(sort_by_price: false preserves insertion order) because it is not a thermostat — it
extends a paired thermostat's capacity for high-load zones.

Cross-Reference Section (SEO / Anti-Cannibalization)

The #shop-by-system section provides 24 internal links grouped by:

  • By Floor Type (8 links) — tile, LVT, engineered wood, laminate, bamboo, nailed
    hardwood, concrete, carpet
  • By Room (8 links) — bathroom, kitchen, bedroom, living room, basement, shower,
    laundry/mudroom, sunroom
  • By System Type (5 links) — mats, cable, custom mats, Prodeso, underlayment

This signals to Google that /floor-heating/thermostats is about controls and the
linked pages are the authoritative source for each floor type / room — preventing keyword
overlap and reinforcing the topical hierarchy under /floor-heating.

The pattern mirrors towel-warmer/controls → /towel-warmer/* cross-references.


Tagline (Short Description)

The component reads item.short_description from the product database and surfaces it as a
muted subtitle beneath each product title in the comparison header:

tagline: presenter.item&.short_description.presence

The tagline row is omitted entirely when no product in the comparison has a short
description, keeping the header clean for legacy SKUs.