|
| 1 | +# Atomic Design Implementation Plan for Rails Frontend |
| 2 | + |
| 3 | +This document outlines a phased approach to implementing Atomic Design principles in PropertyWebBuilder's Rails-rendered frontend (B-themes). |
| 4 | + |
| 5 | +## Goals |
| 6 | + |
| 7 | +1. **Improve reusability** - Share components across all B-themes |
| 8 | +2. **Reduce duplication** - Extract common patterns into reusable partials |
| 9 | +3. **Enhance maintainability** - Clear hierarchy makes code easier to navigate |
| 10 | +4. **Enable isolated testing** - Smaller units are easier to test |
| 11 | +5. **Accelerate theme development** - New themes only override CSS, not structure |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## Phase 1: Directory Structure Setup |
| 16 | + |
| 17 | +### Create Atomic Directories |
| 18 | + |
| 19 | +``` |
| 20 | +app/views/shared/ |
| 21 | +├── atoms/ # Smallest UI primitives |
| 22 | +├── molecules/ # Simple combinations of atoms |
| 23 | +└── organisms/ # Complex, self-contained components |
| 24 | +``` |
| 25 | + |
| 26 | +### Files to Create |
| 27 | + |
| 28 | +| Directory | Purpose | |
| 29 | +|-----------|---------| |
| 30 | +| `shared/atoms/` | Buttons, badges, icons, form inputs, links | |
| 31 | +| `shared/molecules/` | Form groups, price displays, nav items, stat cards | |
| 32 | +| `shared/organisms/` | Property cards, search filters, testimonial cards | |
| 33 | + |
| 34 | +--- |
| 35 | + |
| 36 | +## Phase 2: Extract Atoms |
| 37 | + |
| 38 | +### Priority Atoms to Create |
| 39 | + |
| 40 | +#### 2.1 Button (`_button.html.erb`) |
| 41 | + |
| 42 | +```erb |
| 43 | +<%# app/views/shared/atoms/_button.html.erb %> |
| 44 | +<%# Usage: render "shared/atoms/button", text: "Submit", variant: :primary, size: :md %> |
| 45 | +<% |
| 46 | + variant ||= :primary |
| 47 | + size ||= :md |
| 48 | + type ||= :button |
| 49 | + disabled ||= false |
| 50 | + |
| 51 | + base_classes = "pwb-btn" |
| 52 | + variant_classes = { |
| 53 | + primary: "pwb-btn--primary", |
| 54 | + secondary: "pwb-btn--secondary", |
| 55 | + outline: "pwb-btn--outline", |
| 56 | + ghost: "pwb-btn--ghost" |
| 57 | + } |
| 58 | + size_classes = { |
| 59 | + sm: "pwb-btn--sm", |
| 60 | + md: "pwb-btn--md", |
| 61 | + lg: "pwb-btn--lg" |
| 62 | + } |
| 63 | + |
| 64 | + classes = [base_classes, variant_classes[variant], size_classes[size], local_assigns[:class]].compact.join(" ") |
| 65 | +%> |
| 66 | +<button type="<%= type %>" class="<%= classes %>" <%= "disabled" if disabled %>> |
| 67 | + <%= text %> |
| 68 | +</button> |
| 69 | +``` |
| 70 | + |
| 71 | +#### 2.2 Badge (`_badge.html.erb`) |
| 72 | + |
| 73 | +```erb |
| 74 | +<%# app/views/shared/atoms/_badge.html.erb %> |
| 75 | +<% |
| 76 | + variant ||= :default |
| 77 | + classes = "pwb-badge pwb-badge--#{variant} #{local_assigns[:class]}" |
| 78 | +%> |
| 79 | +<span class="<%= classes %>"><%= text %></span> |
| 80 | +``` |
| 81 | + |
| 82 | +#### 2.3 Icon (`_icon.html.erb`) |
| 83 | + |
| 84 | +```erb |
| 85 | +<%# app/views/shared/atoms/_icon.html.erb %> |
| 86 | +<%# Wraps existing icon helper with consistent classes %> |
| 87 | +<%= icon(name, class: "pwb-icon pwb-icon--#{size || 'md'} #{local_assigns[:class]}") %> |
| 88 | +``` |
| 89 | + |
| 90 | +#### 2.4 Form Input (`_input.html.erb`) |
| 91 | + |
| 92 | +```erb |
| 93 | +<%# app/views/shared/atoms/_input.html.erb %> |
| 94 | +<% |
| 95 | + type ||= :text |
| 96 | + required ||= false |
| 97 | + classes = "pwb-input #{local_assigns[:class]}" |
| 98 | +%> |
| 99 | +<input |
| 100 | + type="<%= type %>" |
| 101 | + name="<%= name %>" |
| 102 | + id="<%= id || name %>" |
| 103 | + value="<%= value %>" |
| 104 | + placeholder="<%= placeholder %>" |
| 105 | + class="<%= classes %>" |
| 106 | + <%= "required" if required %> |
| 107 | +> |
| 108 | +``` |
| 109 | + |
| 110 | +--- |
| 111 | + |
| 112 | +## Phase 3: Extract Molecules |
| 113 | + |
| 114 | +### Priority Molecules to Create |
| 115 | + |
| 116 | +#### 3.1 Form Group (`_form_group.html.erb`) |
| 117 | + |
| 118 | +```erb |
| 119 | +<%# app/views/shared/molecules/_form_group.html.erb %> |
| 120 | +<div class="pwb-form-group"> |
| 121 | + <label for="<%= input_id %>" class="pwb-form-group__label"> |
| 122 | + <%= label_text %> |
| 123 | + <% if required %><span class="pwb-form-group__required">*</span><% end %> |
| 124 | + </label> |
| 125 | + <%= yield %> |
| 126 | + <% if error.present? %> |
| 127 | + <span class="pwb-form-group__error"><%= error %></span> |
| 128 | + <% end %> |
| 129 | +</div> |
| 130 | +``` |
| 131 | + |
| 132 | +#### 3.2 Price Display (`_price_display.html.erb`) |
| 133 | + |
| 134 | +```erb |
| 135 | +<%# app/views/shared/molecules/_price_display.html.erb %> |
| 136 | +<div class="pwb-price-display"> |
| 137 | + <span class="pwb-price-display__value"><%= formatted_price %></span> |
| 138 | + <% if rental %> |
| 139 | + <span class="pwb-price-display__suffix">/<%= I18n.t('common.month') %></span> |
| 140 | + <% end %> |
| 141 | +</div> |
| 142 | +``` |
| 143 | + |
| 144 | +#### 3.3 Property Stats (`_property_stats.html.erb`) |
| 145 | + |
| 146 | +```erb |
| 147 | +<%# app/views/shared/molecules/_property_stats.html.erb %> |
| 148 | +<div class="pwb-prop-stats"> |
| 149 | + <span class="pwb-prop-stats__item"> |
| 150 | + <%= render "shared/atoms/icon", name: "bed", size: "sm" %> |
| 151 | + <%= bedrooms %> <%= I18n.t('properties.beds') %> |
| 152 | + </span> |
| 153 | + <span class="pwb-prop-stats__item"> |
| 154 | + <%= render "shared/atoms/icon", name: "bath", size: "sm" %> |
| 155 | + <%= bathrooms %> <%= I18n.t('properties.baths') %> |
| 156 | + </span> |
| 157 | + <% if garages.present? && garages > 0 %> |
| 158 | + <span class="pwb-prop-stats__item"> |
| 159 | + <%= render "shared/atoms/icon", name: "car", size: "sm" %> |
| 160 | + <%= garages %> <%= I18n.t('properties.garage') %> |
| 161 | + </span> |
| 162 | + <% end %> |
| 163 | +</div> |
| 164 | +``` |
| 165 | + |
| 166 | +#### 3.4 Nav Link (`_nav_link.html.erb`) |
| 167 | + |
| 168 | +```erb |
| 169 | +<%# app/views/shared/molecules/_nav_link.html.erb %> |
| 170 | +<li class="pwb-nav__item <%= 'pwb-nav__item--active' if active %>"> |
| 171 | + <%= link_to path, class: "pwb-nav__link", target: target do %> |
| 172 | + <% if icon.present? %> |
| 173 | + <%= render "shared/atoms/icon", name: icon, size: "sm" %> |
| 174 | + <% end %> |
| 175 | + <%= title %> |
| 176 | + <% end %> |
| 177 | +</li> |
| 178 | +``` |
| 179 | + |
| 180 | +--- |
| 181 | + |
| 182 | +## Phase 4: Extract Organisms |
| 183 | + |
| 184 | +### Priority Organisms to Create |
| 185 | + |
| 186 | +#### 4.1 Property Card (`_property_card.html.erb`) |
| 187 | + |
| 188 | +Extract from existing property listing views: |
| 189 | + |
| 190 | +```erb |
| 191 | +<%# app/views/shared/organisms/_property_card.html.erb %> |
| 192 | +<article class="pwb-prop-card" data-property-id="<%= property.id %>"> |
| 193 | + <a href="<%= property_path(property) %>" class="pwb-prop-card__link"> |
| 194 | + <div class="pwb-prop-card__image"> |
| 195 | + <%= image_tag property.primary_photo_url, alt: property.title, loading: "lazy" %> |
| 196 | + <% if property.highlighted? %> |
| 197 | + <%= render "shared/atoms/badge", text: I18n.t('properties.featured'), variant: :primary %> |
| 198 | + <% end %> |
| 199 | + <div class="pwb-prop-card__price"> |
| 200 | + <%= render "shared/molecules/price_display", |
| 201 | + formatted_price: property.formatted_price, |
| 202 | + rental: property.for_rent? && !property.for_sale? %> |
| 203 | + </div> |
| 204 | + </div> |
| 205 | + <div class="pwb-prop-card__body"> |
| 206 | + <h3 class="pwb-prop-card__title"><%= property.title %></h3> |
| 207 | + <%= render "shared/molecules/property_stats", |
| 208 | + bedrooms: property.count_bedrooms, |
| 209 | + bathrooms: property.count_bathrooms, |
| 210 | + garages: property.count_garages %> |
| 211 | + </div> |
| 212 | + </a> |
| 213 | +</article> |
| 214 | +``` |
| 215 | + |
| 216 | +#### 4.2 Testimonial Card (`_testimonial_card.html.erb`) |
| 217 | + |
| 218 | +```erb |
| 219 | +<%# app/views/shared/organisms/_testimonial_card.html.erb %> |
| 220 | +<blockquote class="pwb-testimonial"> |
| 221 | + <div class="pwb-testimonial__content"> |
| 222 | + <p class="pwb-testimonial__quote"><%= testimonial.content %></p> |
| 223 | + </div> |
| 224 | + <footer class="pwb-testimonial__footer"> |
| 225 | + <% if testimonial.photo_url.present? %> |
| 226 | + <%= image_tag testimonial.photo_url, alt: testimonial.name, class: "pwb-testimonial__avatar" %> |
| 227 | + <% end %> |
| 228 | + <div class="pwb-testimonial__author"> |
| 229 | + <cite class="pwb-testimonial__name"><%= testimonial.name %></cite> |
| 230 | + <% if testimonial.role.present? %> |
| 231 | + <span class="pwb-testimonial__role"><%= testimonial.role %></span> |
| 232 | + <% end %> |
| 233 | + </div> |
| 234 | + </footer> |
| 235 | +</blockquote> |
| 236 | +``` |
| 237 | + |
| 238 | +#### 4.3 Search Filters (`_search_filters.html.erb`) |
| 239 | + |
| 240 | +Extract from existing search form implementation. |
| 241 | + |
| 242 | +--- |
| 243 | + |
| 244 | +## Phase 5: Refactor Existing Partials |
| 245 | + |
| 246 | +### Files to Migrate |
| 247 | + |
| 248 | +| Current Location | Target | Priority | |
| 249 | +|------------------|--------|----------| |
| 250 | +| `pwb/_header.html.erb` | Keep as organism, extract molecules | High | |
| 251 | +| `pwb/_footer.html.erb` | Keep as organism, extract molecules | High | |
| 252 | +| Property listing cards | `shared/organisms/_property_card.html.erb` | High | |
| 253 | +| `page_parts/heroes/*` | Keep, use atoms/molecules internally | Medium | |
| 254 | +| `page_parts/features/*` | Keep, use atoms/molecules internally | Medium | |
| 255 | +| `page_parts/testimonials/*` | Keep, use atoms/molecules internally | Medium | |
| 256 | + |
| 257 | +--- |
| 258 | + |
| 259 | +## Phase 6: CSS Organization |
| 260 | + |
| 261 | +### BEM Class Naming |
| 262 | + |
| 263 | +All atomic components use BEM with `pwb-` prefix: |
| 264 | + |
| 265 | +```css |
| 266 | +/* Atoms */ |
| 267 | +.pwb-btn { } |
| 268 | +.pwb-btn--primary { } |
| 269 | +.pwb-btn--lg { } |
| 270 | + |
| 271 | +/* Molecules */ |
| 272 | +.pwb-form-group { } |
| 273 | +.pwb-form-group__label { } |
| 274 | +.pwb-form-group__error { } |
| 275 | + |
| 276 | +/* Organisms */ |
| 277 | +.pwb-prop-card { } |
| 278 | +.pwb-prop-card__image { } |
| 279 | +.pwb-prop-card__title { } |
| 280 | +``` |
| 281 | + |
| 282 | +### Create Component CSS Files |
| 283 | + |
| 284 | +``` |
| 285 | +app/views/pwb/custom_css/ |
| 286 | +├── atoms/ |
| 287 | +│ ├── _buttons.css.erb |
| 288 | +│ ├── _badges.css.erb |
| 289 | +│ └── _icons.css.erb |
| 290 | +├── molecules/ |
| 291 | +│ ├── _form_group.css.erb |
| 292 | +│ ├── _price_display.css.erb |
| 293 | +│ └── _property_stats.css.erb |
| 294 | +└── organisms/ |
| 295 | + ├── _property_card.css.erb |
| 296 | + └── _testimonial.css.erb |
| 297 | +``` |
| 298 | + |
| 299 | +--- |
| 300 | + |
| 301 | +## Phase 7: Testing Strategy |
| 302 | + |
| 303 | +### ViewComponent Migration (Optional) |
| 304 | + |
| 305 | +For complex organisms, consider migrating to [ViewComponent](https://viewcomponent.org/): |
| 306 | + |
| 307 | +```ruby |
| 308 | +# app/components/property_card_component.rb |
| 309 | +class PropertyCardComponent < ViewComponent::Base |
| 310 | + def initialize(property:, show_badge: true) |
| 311 | + @property = property |
| 312 | + @show_badge = show_badge |
| 313 | + end |
| 314 | +end |
| 315 | +``` |
| 316 | + |
| 317 | +### Partial Testing |
| 318 | + |
| 319 | +```ruby |
| 320 | +# spec/views/shared/atoms/button_spec.rb |
| 321 | +RSpec.describe "shared/atoms/_button.html.erb" do |
| 322 | + it "renders primary button" do |
| 323 | + render partial: "shared/atoms/button", locals: { text: "Click", variant: :primary } |
| 324 | + expect(rendered).to have_css(".pwb-btn.pwb-btn--primary", text: "Click") |
| 325 | + end |
| 326 | +end |
| 327 | +``` |
| 328 | + |
| 329 | +--- |
| 330 | + |
| 331 | +## Implementation Timeline |
| 332 | + |
| 333 | +| Phase | Estimated Effort | Dependencies | |
| 334 | +|-------|------------------|--------------| |
| 335 | +| Phase 1: Directory Setup | 1 hour | None | |
| 336 | +| Phase 2: Extract Atoms | 4-6 hours | Phase 1 | |
| 337 | +| Phase 3: Extract Molecules | 4-6 hours | Phase 2 | |
| 338 | +| Phase 4: Extract Organisms | 8-12 hours | Phase 3 | |
| 339 | +| Phase 5: Refactor Existing | 12-16 hours | Phase 4 | |
| 340 | +| Phase 6: CSS Organization | 4-6 hours | Phase 4 | |
| 341 | +| Phase 7: Testing | 8-12 hours | All phases | |
| 342 | + |
| 343 | +**Total Estimated Effort**: 40-60 hours (spread over multiple sprints) |
| 344 | + |
| 345 | +--- |
| 346 | + |
| 347 | +## Success Metrics |
| 348 | + |
| 349 | +- [ ] All B-themes share common atoms/molecules |
| 350 | +- [ ] New theme creation requires only CSS changes |
| 351 | +- [ ] 80%+ code reuse across themes |
| 352 | +- [ ] Component preview/documentation available (Lookbook or similar) |
| 353 | +- [ ] Reduced bug reports related to UI inconsistencies |
| 354 | + |
| 355 | +--- |
| 356 | + |
| 357 | +## Related Documents |
| 358 | + |
| 359 | +- [Design Tokens](./DESIGN_TOKENS.md) - Token values used by atomic components |
| 360 | +- [Frontend Standards](../FRONTEND_STANDARDS.md) - BEM naming conventions |
0 commit comments