Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Refactoring Volatile Views into Cohesive Compon...

Avatar for Jeremy Smith Jeremy Smith
September 10, 2025
25

Refactoring Volatile Views into Cohesive Components (v. 2)

It's easy for models to grow out of control, accumulating methods, attributes and responsibilities. But you know what can be worse? The view layer: a veritable wasteland of messy markup and leaking logic. Let's look at how to refactor that mess into clean, cohesive components with ViewComponent.

(This is an revised and updated version of the original talk from 2024.)

Companion repo: https://github.com/jeremysmithco/volatile-crm

Avatar for Jeremy Smith

Jeremy Smith

September 10, 2025
Tweet

Transcript

  1. Complexity Variation More options or variants for an attribute Proliferation

    More instances throughout the system Accumulation More attributes or responsibilities
  2. Dimensions of View Complexity Browsers/Clients Viewports Device Features Theming/Whitelabeling Dark/Light

    Mode Accessibility Internationalization SEO/Open Graph Authorization Entitlements Feature Flags Framework/Version Changes Execution Context Testing Affordances Design Systems/Tokens
  3. <%= tag.nav(class: "flex justify - between space - x-2 items

    - start border - b-2 border - gray-400") do %> <%= tag.div(class: "flex - mb-0.5 space - x-2") do %> <%= link_to root_path, class: "group whitespace - nowrap flex items - center space - x-1 rounded - md rounded - b - none leading - none py-3 px-3 bg - gray-300 hover:bg - gray-400 border - b-2 # { current_page?(tab_path) ? "border - b - red-400" : "border - b - gray-400"}" do %> <%= tag.span(render("icons/home"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Dashboard", class: "text - gray-600 group - hover:text - gray-700 #{ "font - semibold" if current_page?(tab_path)}") %> <% end %> <%= link_to contacts_path, class: "group whitespace - nowrap flex items - center space - x-1 rounded - md rounded - b - none leading - none py-3 px-3 bg - gray-300 hover:bg - gray-400 border - b-2 #{ current_page?(contacts_path) ? "border - b - red-400" : "border - b - gray-400"}" do % <%= tag.span(render("icons/users"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Contacts", class: "text - gray-600 group - hover:text - gray-700 #{ "font - semibold" if current_page?(contacts_path)}") %> <%= tag.span(current_account.contacts, class: "text - xs leading - none p-1 rounded - md bg - gray-50 text - gray-600") %> <% end %> <%= link_to companies_path, class: "group whitespace - nowrap flex items - center space - x-1 rounded - md rounded - b - none leading - none py-3 px-3 bg - gray-300 hover:bg - gray-400 border - b-2 # { current_page?(companies_path) ? "border - b - red-400" : "border - b - gray-400"}" do <%= tag.span(render("icons/building - office"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Companies", class: "text - gray-600 group - hover:text - gray-700 #{ "font - semibold" if current_page?(companies_path)}") %> <%= tag.span(current_account.companies, class: "text - xs leading - none p-1 rounded - md bg - gray-50 text - gray-600") %> <% end %> <%= link_to tasks_path, class: "group whitespace - nowrap flex items - center space - x-1 rounded - md rounded - b - none leading - none py-3 px-3 bg - gray-300 hover:bg - gray-400 border - b-2 #{ current_page?(tasks_path) ? "border - b - red-400" : "border - b - gray-400"}" do %> <%= tag.span(render("icons/pencil - square"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Tasks", class: "text - gray-600 group - hover:text - gray-700 # { "font - semibold" if current_page?(tasks_path)}") %> <%= tag.span(current_account.tasks, class: "text - xs leading - none p-1 rounded - md # { current_account.tasks > 15 ? "bg - red-50 text - red-600" : "bg - gray-50 text - gray-600"}") %> <% end %> <% if current_account.plan = = "pro" %> <%= link_to reports_path, class: "group whitespace - nowrap flex items - center space - x-1 rounded - md rounded - b - none leading - none py-3 px-3 bg - gray-300 hover:bg - gray-400 border - b-2 # { current_page?(reports_path) ? "border - b - red-400" : "border - b - gray-400"}" do % <%= tag.span(render("icons/chart - bar"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Reports", class: "text - gray-600 group - hover:text - gray-700 #{ "font - semibold" if current_page?(reports_path)}") %> <% end %> <% else %> <%= tag.span data: { controller: "tooltip", tooltip_content_value: "Reports are only available on the Pro plan." }, class: "group whitespace - nowrap flex items - center space - x-1 rounded - md rounded - b - none leading - none py-3 px-3 bg - gray-100 border - b-2 border - <%= tag.span(render("icons/chart - bar"), class: "text - gray-300") %> <%= tag.span("Reports", class: "text - gray-400") %> <% end %> <% end %> <% if current_user.admin? %> <%= tag.span data: { controller: "toggle", toggle_toggle_class: "hidden" }, class: "relative" do %> <%= tag.button(class: "h - full group whitespace - nowrap flex items - center space - x-1 rounded - md rounded - b - none leading - none py-3 px-3 bg - gray-300 hover:bg - gray-400 border - b-2 #{ current_page?(settings_path) ? "border - b - red-400" : "border - b - gray-400"}", data <%= tag.span(render("icons/cog-6-tooth"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Settings", class: "text - gray-600 group - hover:text - gray-700 # { "font - semibold" if current_page?(settings_path)}") %> <%= tag.span(render("icons/chevron - down"), class: "text - gray-600 group - hover:text - gray-700") %> <% end %> <%= tag.div(class: "hidden z-40 absolute mt-1 w-60 rounded - md border-2 border - gray-300 shadow - lg py-1 bg - gray-200 divide - y divide - gray-200", data: { toggle_target: "toggleable" }) do %> <%= link_to "Collaborators", settings_collaborators_path, class: "block w - full text - left py-1.5 px-3 text - gray-500 hover:text - gray-600 hover:bg - gray-300" %> <%= link_to "Notifications", settings_notifications_path, class: "block w - full text - left py-1.5 px-3 text - gray-500 hover:text - gray-600 hover:bg - gray-300" %> <% end %> <% end %> <% end %> <% end %> <% if current_account.enabled_feature?(:search) %> <%= tag.div(class: "flex - mb-0.5 space - x-2") do %> <%= form_tag searches_path, method: :get do %> <%= text_field_tag :query, "", placeholder: "Search", autocomplete: "off", class: "w - full text - sm border-2 border - gray-300 bg - gray-50 rounded - md shadow - inner placeholder:text - gray-400" %> <% end %> <% end %> <% end %> <% end %>
  4. Types of Components Layout Used for composing and positioning Model-Speci

    fi c Coupled to domain models Utility General-purpose, common UI patterns
  5. class AlertComponent < ApplicationComponent renders_one :message def initialize(type:, title: nil,

    icon: nil) @type, @title, @icon = type, title, icon end CLASSES = { notice: { background: "bg - emerald-200", icon: "text - emerald-700", text: "text - emerald-800" }, info: { background: "bg - cyan-200", icon: "text - cyan-700", text: "text - cyan-800" }, warning: { background: "bg - yellow-200", icon: "text - yellow-700", text: "text - yellow-800" }, alert: { background: "bg - rose-200", icon: "text - rose-700", text: "text - rose-800" } }.freeze private attr_reader :type, :title, :icon def color(element) CLASSES.dig(type, element) end def wrapper_classes class_names(color(:background), "flex items - center gap-2 p-3 mb-4 rounded - md") end …
  6. class AlertComponent < ApplicationComponent renders_one :message def initialize(type:, title: nil,

    icon: nil) @type, @title, @icon = type, title, icon end CLASSES = { notice: { background: "bg info: { background: "bg warning: { background: "bg alert: { background: "bg }.freeze private attr_reader :type, :title, :icon def color(element) CLASSES.dig(type, element) end def wrapper_classes class_names(color(:background), "flex items end … def initialize(type:, title: nil, icon: nil) @type, @title, @icon = type, title, icon end
  7. class AlertComponent < ApplicationComponent renders_one :message def initialize(type:, title: nil,

    icon: nil) @type, @title, @icon = type, title, icon end CLASSES = { notice: { background: "bg info: { background: "bg warning: { background: "bg alert: { background: "bg }.freeze private attr_reader :type, :title, :icon def color(element) CLASSES.dig(type, element) end def wrapper_classes class_names(color(:background), "flex items end … renders_one :message
  8. class AlertComponent < ApplicationComponent renders_one :message def initialize(type:, title: nil,

    icon: nil) @type, @title, @icon = type, title, icon end CLASSES = { notice: { background: "bg info: { background: "bg warning: { background: "bg alert: { background: "bg }.freeze private attr_reader :type, :title, :icon def color(element) CLASSES.dig(type, element) end def wrapper_classes class_names(color(:background), "flex items end … CLASSES = { notice: { background: "bg - emerald-200", icon: "text - emerald-700", text: "text - emerald-800" }, info: { background: "bg - cyan-200", icon: "text - cyan-700", text: "text - cyan-800" }, warning: { background: "bg - yellow-200", icon: "text - yellow-700", text: "text - yellow-800" }, alert: { background: "bg - rose-200", icon: "text - rose-700", text: "text - rose-800" } }.freeze
  9. <%= tag.div(class: wrapper_classes) do %> <%= tag.div(render("icons/ #{ icon}"), class:

    icon_classes) if icon.present? %> <%= tag.div do %> <%= tag.h3(title, class: title_classes) if title.present? %> <%= tag.p(message, class: message_classes) %> <% end %> <% end %>
  10. <%= tag.div(class: wrapper_classes) do %> <%= tag.div(render("icons/ <%= tag.div do

    %> <%= tag.h3(title, class: title_classes) if title.present? %> <%= tag.p(message, class: message_classes) %> <% end %> <% end %> tag.div tag.div tag.div tag.h3 tag.p
  11. <%= tag.div(class: wrapper_classes) do %> <%= tag.div(render("icons/ <%= tag.div do

    %> <%= tag.h3(title, class: title_classes) if title.present? %> <%= tag.p(message, class: message_classes) %> <% end %> <% end %> wrapper_classes icon icon_classes icon title title_classes title message message_classes
  12. <%= tag.div(class: wrapper_classes) do %> <%= tag.div(render("icons/ <%= tag.div do

    %> <%= tag.h3(title, class: title_classes) if title.present? %> <%= tag.p(message, class: message_classes) %> <% end %> <% end %> if icon.present? if title.present?
  13. <%= render AlertComponent.new(type: :notice) .with_message_content("Contact address successfully updated.") %> <%=

    render AlertComponent.new(type: :info, icon: "cake") .with_message_content("John’s birthday is tomorrow. Don’t forget to send him a message!") %> <%= render AlertComponent.new(type: :alert, title: "Danger Zone") do |c| %> <%= c.with_message do %> Please be careful in the section below, as these actions are <strong>not reversible </ strong>! <% end %> <% end %> <%= render AlertComponent.new(type: :warning, icon: "information - circle", title: "Contact Limit Reached") do |c| %> <%= c.with_message do %> You’ve reached the contact limit for your plan. Please <%= link_to "upgrade", root_path, class: "underline" %> to add more contacts. <% end %> <% end %>
  14. <%= render AlertComponent.new(type: :notice) .with_message_content("Contact address successfully updated.") %> <%=

    render AlertComponent.new(type: :info, icon: "cake") .with_message_content("John’s birthday is tomorrow. Don’t forget to send him a message!") %> <%= render AlertComponent.new(type: :alert, title: "Danger Zone") do |c| %> <%= c.with_message do %> Please be careful in the section below, as these actions are <strong>not reversible <% end %> <% end %> <%= render AlertComponent.new(type: :warning, icon: "information title: "Contact Limit Reached") do |c| %> <%= c.with_message do %> You’ve reached the contact limit for your plan. Please <%= link_to "upgrade", root_path, class: "underline" %> to add more contacts. <% end %> <% end %> .with_message_content("Contact address successfully updated.") .with_message_content("John’s birthday is tomorrow. Don’t forget to send him a message!") <%= c.with_message do %> Please be careful in the section below, as these actions are <strong>not reversible </ strong>! <% end %> <%= c.with_message do %> You’ve reached the contact limit for your plan. Please <%= link_to "upgrade", root_path, class: "underline" %> to add more contacts. <% end %>
  15. class PageLayouts :: BasicComponent < ApplicationComponent renders_one :parent, -> (title:,

    link:) do safe_join([ link_to(title, link, class: "text - cyan-500"), tag.div(render("icons/divider"), class: divider_classes) ]) end renders_many :actions, types: { link: "LinkActionComponent", button: "ButtonActionComponent" } renders_one :body def initialize(title:) @title = title end private attr_reader :title …
  16. class PageLayouts renders_one :parent, safe_join([ link_to(title, link, class: "text tag.div(render("icons/divider"),

    class: divider_classes) ]) end renders_many :actions, types: { link: "LinkActionComponent", button: "ButtonActionComponent" } renders_one :body def initialize(title:) @title = title end private attr_reader :title … renders_one :body def initialize(title:) @title = title end
  17. class PageLayouts renders_one :parent, safe_join([ link_to(title, link, class: "text tag.div(render("icons/divider"),

    class: divider_classes) ]) end renders_many :actions, types: { link: "LinkActionComponent", button: "ButtonActionComponent" } renders_one :body def initialize(title:) @title = title end private attr_reader :title … renders_one :parent, -> (title:, link:) do safe_join([ link_to(title, link, class: "text - cyan-500"), tag.div(render("icons/divider"), class: divider_classes) ]) end
  18. class PageLayouts renders_one :parent, safe_join([ link_to(title, link, class: "text tag.div(render("icons/divider"),

    class: divider_classes) ]) end renders_many :actions, types: { link: "LinkActionComponent", button: "ButtonActionComponent" } renders_one :body def initialize(title:) @title = title end private attr_reader :title … renders_many :actions, types: { link: "LinkActionComponent", button: "ButtonActionComponent" }
  19. class LinkActionComponent < ApplicationComponent def initialize(text:, path:, colors: "bg -

    cyan-500 hover:bg - cyan-700") @text, @path, @colors = text, path, colors end def call link_to(text, path, class: "block px-3 py-1 text - white rounded - md … #{ colors}") end private attr_reader :text, :path, :colors end class ButtonActionComponent < ApplicationComponent def initialize(text:, path:, method: :post, colors: "bg - cyan-500 hover:bg - cyan-700") @text, @path, @method, @colors = text, path, method, colors end def call button_to(text, path, method: method, class: "block px-3 py-1 text - white rounded - md … #{ colors}") end …
  20. class LinkActionComponent < ApplicationComponent def initialize(text:, path:, colors: "bg @text,

    @path, @colors = text, path, colors end def call link_to(text, path, class: "block px-3 py-1 text end private attr_reader :text, :path, :colors end class ButtonActionComponent < ApplicationComponent def initialize(text:, path:, method: :post, colors: "bg @text, @path, @method, @colors = text, path, method, colors end def call button_to(text, path, method: method, class: "block px-3 py-1 text end … def call link_to(text, path, class: "block px-3 py-1 text - white rounded - md … #{ colors}") end def call button_to(text, path, method: method, class: "block px-3 py-1 text - white rounded - md … #{ colors}") end
  21. <%= tag.div(class: wrapper_classes) do %> <%= tag.div(class: header_classes) do %>

    <%= tag.div(class: title_classes) do %> <%= parent %> <%= tag.div(title) %> <% end %> <% if actions? %> <%= tag.div(class: action_classes) do %> <% actions.each do |action| %> <%= action %> <% end %> <% end %> <% end %> <% end %> <%= tag.div(body, class: body_classes) %> <% end %>
  22. <%= tag.div(class: wrapper_classes) do %> <%= tag.div(class: header_classes) do %>

    <%= tag.div(class: title_classes) do %> <%= parent %> <%= tag.div(title) %> <% end %> <% if actions? %> <%= tag.div(class: action_classes) do %> <% actions.each do |action| %> <%= action %> <% end %> <% end %> <% end %> <% end %> <%= tag.div(body, class: body_classes) %> <% end %> <% if actions? %> <% actions.each do |action| %> <%= action %> <% end %> <% end %>
  23. <%= render PageLayouts :: BasicComponent.new(title: "Companies") do |c| %> <%=

    c.with_action_link(text: "New", path: new_company_path) %> <%= c.with_action_link(text: "Settings", path: new_company_path, colors: "bg - gray-500 hover:bg - gray-700") %> <%= c.with_body do %> <%= render "table", companies: @companies %> <% end %> <% end %> <%= render PageLayouts :: BasicComponent.new(title: @company.name) do |c| %> <%= c.with_parent(title: "Companies", link: companies_path) %> <%= c.with_action_button(text: "Duplicate", path: new_company_path) %> <%= c.with_action_button(text: "Delete", path: company_path(@company), method: :delete, colors: "bg - rose-500 hover:bg - rose-700") %> <%= c.with_body do %> <%= render "form", company: @company %> <% end %> <% end %>
  24. <%= render PageLayouts <%= c.with_action_link(text: "New", path: new_company_path) %> <%=

    c.with_action_link(text: "Settings", path: new_company_path, colors: "bg <%= c.with_body do %> <%= render "table", companies: @companies %> <% end %> <% end %> <%= render PageLayouts <%= c.with_parent(title: "Companies", link: companies_path) %> <%= c.with_action_button(text: "Duplicate", path: new_company_path) %> <%= c.with_action_button(text: "Delete", path: company_path(@company), method: :delete, colors: "bg <%= c.with_body do %> <%= render "form", company: @company %> <% end %> <% end %> title: "Companies" title: @company.name
  25. <%= render PageLayouts <%= c.with_action_link(text: "New", path: new_company_path) %> <%=

    c.with_action_link(text: "Settings", path: new_company_path, colors: "bg <%= c.with_body do %> <%= render "table", companies: @companies %> <% end %> <% end %> <%= render PageLayouts <%= c.with_parent(title: "Companies", link: companies_path) %> <%= c.with_action_button(text: "Duplicate", path: new_company_path) %> <%= c.with_action_button(text: "Delete", path: company_path(@company), method: :delete, colors: "bg <%= c.with_body do %> <%= render "form", company: @company %> <% end %> <% end %> <%= c.with_parent(title: "Companies", link: companies_path) %>
  26. <%= render PageLayouts <%= c.with_action_link(text: "New", path: new_company_path) %> <%=

    c.with_action_link(text: "Settings", path: new_company_path, colors: "bg <%= c.with_body do %> <%= render "table", companies: @companies %> <% end %> <% end %> <%= render PageLayouts <%= c.with_parent(title: "Companies", link: companies_path) %> <%= c.with_action_button(text: "Duplicate", path: new_company_path) %> <%= c.with_action_button(text: "Delete", path: company_path(@company), method: :delete, colors: "bg <%= c.with_body do %> <%= render "form", company: @company %> <% end %> <% end %> <%= c.with_action_link(text: "New", path: new_company_path) %> <%= c.with_action_link(text: "Settings", path: new_company_path, colors: "bg - gray-500 hover:bg - gray-700") %> <%= c.with_action_button(text: "Duplicate", path: new_company_path) %> <%= c.with_action_button(text: "Delete", path: company_path(@company), method: :delete, colors: "bg - rose-500 hover:bg - rose-700") %>
  27. <%= render PageLayouts <%= c.with_action_link(text: "New", path: new_company_path) %> <%=

    c.with_action_link(text: "Settings", path: new_company_path, colors: "bg <%= c.with_body do %> <%= render "table", companies: @companies %> <% end %> <% end %> <%= render PageLayouts <%= c.with_parent(title: "Companies", link: companies_path) %> <%= c.with_action_button(text: "Duplicate", path: new_company_path) %> <%= c.with_action_button(text: "Delete", path: company_path(@company), method: :delete, colors: "bg <%= c.with_body do %> <%= render "form", company: @company %> <% end %> <% end %> <%= c.with_body do %> <%= render "table", companies: @companies %> <% end %> <%= c.with_body do %> <%= render "form", company: @company %> <% end %>
  28. class ContactCardComponent < ApplicationComponent with_collection_parameter :contact renders_one :avatar, AvatarComponent def

    initialize(contact:) @contact = contact end def default_avatar AvatarComponent.new(contact: contact) end private attr_reader :contact def wrapper_classes "flex flex - col block bg - white shadow rounded - md p-4" end …
  29. class ContactCardComponent < ApplicationComponent with_collection_parameter :contact renders_one :avatar, AvatarComponent def

    initialize(contact:) @contact = contact end def default_avatar AvatarComponent.new(contact: contact) end private attr_reader :contact def wrapper_classes "flex flex end … def initialize(contact:) @contact = contact end
  30. class ContactCardComponent < ApplicationComponent with_collection_parameter :contact renders_one :avatar, AvatarComponent def

    initialize(contact:) @contact = contact end def default_avatar AvatarComponent.new(contact: contact) end private attr_reader :contact def wrapper_classes "flex flex end … with_collection_parameter :contact
  31. class ContactCardComponent < ApplicationComponent with_collection_parameter :contact renders_one :avatar, AvatarComponent def

    initialize(contact:) @contact = contact end def default_avatar AvatarComponent.new(contact: contact) end private attr_reader :contact def wrapper_classes "flex flex end … renders_one :avatar, AvatarComponent def default_avatar AvatarComponent.new(contact: contact) end
  32. class AvatarComponent < ApplicationComponent def initialize(contact:) @contact = contact end

    def call if contact_avatar? image_tag(contact_avatar, class: "flex shrink-0 size-20 rounded - full …", alt: contact.full_name) else tag.div(class: "size-20 rounded - full #{ fallback_color} flex shrink-0 justify - center …") do tag.div(contact.first_initial, class: "text-5xl text - white text - center") end end end private attr_reader :contact def contact_avatar? contact.avatar.attached? && contact.avatar.variable? end …
  33. <%= link_to(contact_path(contact), class: wrapper_classes) do %> <%= tag.div(class: body_classes) do

    %> <%= tag.div do %> <%= tag.div(class: header_classes) do %> <%= tag.span(contact.full_name, class: name_classes) %> <%= tag.span(contact.company.name) %> <% end %> <%= detail(icon: "map - pin", field: contact.location) %> <%= detail(icon: "envelope", field: contact.email, color: "text - cyan-500") %> <%= detail(icon: "phone", field: contact.phone) %> <% end %> <%= avatar %> <% end %> <% if contact.social? %> <%= tag.div(class: footer_classes) do %> <%= social(icon: "linkedin", field: contact.linkedin) %> <%= social(icon: "github", field: contact.github) %> <%= social(icon: "twitter", field: contact.twitter) %> <% end %> <% end %> <% end %>
  34. <%= link_to(contact_path(contact), class: wrapper_classes) do %> <%= tag.div(class: body_classes) do

    %> <%= tag.div do %> <%= tag.div(class: header_classes) do %> <%= tag.span(contact.full_name, class: name_classes) %> <%= tag.span(contact.company.name) %> <% end %> <%= detail(icon: "map <%= detail(icon: "envelope", field: contact.email, color: "text <%= detail(icon: "phone", field: contact.phone) %> <% end %> <%= avatar %> <% end %> <% if contact.social? %> <%= tag.div(class: footer_classes) do %> <%= social(icon: "linkedin", field: contact.linkedin) %> <%= social(icon: "github", field: contact.github) %> <%= social(icon: "twitter", field: contact.twitter) %> <% end %> <% end %> <% end %> <%= link_to(contact_path(contact), class: wrapper_classes) do %> <% end %>
  35. <%= link_to(contact_path(contact), class: wrapper_classes) do %> <%= tag.div(class: body_classes) do

    %> <%= tag.div do %> <%= tag.div(class: header_classes) do %> <%= tag.span(contact.full_name, class: name_classes) %> <%= tag.span(contact.company.name) %> <% end %> <%= detail(icon: "map <%= detail(icon: "envelope", field: contact.email, color: "text <%= detail(icon: "phone", field: contact.phone) %> <% end %> <%= avatar %> <% end %> <% if contact.social? %> <%= tag.div(class: footer_classes) do %> <%= social(icon: "linkedin", field: contact.linkedin) %> <%= social(icon: "github", field: contact.github) %> <%= social(icon: "twitter", field: contact.twitter) %> <% end %> <% end %> <% end %> <%= avatar %>
  36. <%= link_to(contact_path(contact), class: wrapper_classes) do %> <%= tag.div(class: body_classes) do

    %> <%= tag.div do %> <%= tag.div(class: header_classes) do %> <%= tag.span(contact.full_name, class: name_classes) %> <%= tag.span(contact.company.name) %> <% end %> <%= detail(icon: "map <%= detail(icon: "envelope", field: contact.email, color: "text <%= detail(icon: "phone", field: contact.phone) %> <% end %> <%= avatar %> <% end %> <% if contact.social? %> <%= tag.div(class: footer_classes) do %> <%= social(icon: "linkedin", field: contact.linkedin) %> <%= social(icon: "github", field: contact.github) %> <%= social(icon: "twitter", field: contact.twitter) %> <% end %> <% end %> <% end %> <%= detail(icon: "map - pin", field: contact.location) %> <%= detail(icon: "envelope", field: contact.email, color: "text - cyan-500") %> <%= detail(icon: "phone", field: contact.phone) %> <% if contact.social? %> <%= tag.div(class: footer_classes) do %> <%= social(icon: "linkedin", field: contact.linkedin) %> <%= social(icon: "github", field: contact.github) %> <%= social(icon: "twitter", field: contact.twitter) %> <% end %> <% end %>
  37. def detail(icon:, field:, color: "text - gray-500") return unless field.present?

    tag.div(class: "flex space - x-2 items - center mt-2") do safe_join([ tag.span(render("icons/ #{ icon}"), class: "text - gray-400"), tag.div(field, class: "text - sm break - all #{ color}") ]) end end def social(icon:, field:) return unless field.present? tag.span(render("icons/ #{ icon}"), class: "text - gray-400") end
  38. <%= tag.div(class: "flex flex - col space - y-4 max

    - w - lg mb-8") do %> <%= render ContactCardComponent.with_collection(@contacts) %> <% end %>
  39. <%= tag.nav(class: "flex space - x-2 …") do %> <%=

    link_to "Dashboard", root_path, class: "whitespace - nowrap flex …" %> <%= link_to "Contacts", contacts_path, class: "whitespace - nowrap flex …" %> <%= link_to "Companies", companies_path, class: "whitespace - nowrap flex …" %> <%= link_to "Tasks", tasks_path, class: "whitespace - nowrap flex …" %> <% end %>
  40. <%= tag.nav(class: "flex space - x-2 …") do %> <%=

    link_to "Dashboard", root_path, class: "whitespace - nowrap flex …" %> <%= link_to "Contacts", contacts_path, class: "whitespace - nowrap flex …" %> <%= link_to "Companies", companies_path, class: "whitespace - nowrap flex …" %> <%= link_to "Tasks", tasks_path, class: "whitespace - nowrap flex …" %> <% end %>
  41. <%= tag.nav(class: "flex space - x-2 …") do %> <%=

    link_to root_path, class: "group whitespace - nowrap …" do %> <%= tag.span(render("icons/home"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Dashboard", class: "text - gray-600 group - hover:text - gray-700") %> <% end %> <%= link_to contacts_path, class: "group whitespace - nowrap …" do %> <%= tag.span(render("icons/users"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Contacts", class: "text - gray-600 group - hover:text - gray-700") %> <% end %> <%= link_to companies_path, class: "group whitespace - nowrap …" do %> <%= tag.span(render("icons/building - office"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Companies", class: "text - gray-600 group - hover:text - gray-700") %> <% end %> <%= link_to tasks_path, class: "group whitespace - nowrap …" do %> <%= tag.span(render("icons/pencil - square"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Tasks", class: "text - gray-600 group - hover:text - gray-700") %> <% end %> <% end %>
  42. <%= tag.nav(class: "flex space <%= link_to root_path, class: "group whitespace

    <%= tag.span(render("icons/home"), class: "text <%= tag.span("Dashboard", class: "text <% end %> <%= link_to contacts_path, class: "group whitespace <%= tag.span(render("icons/users"), class: "text <%= tag.span("Contacts", class: "text <% end %> <%= link_to companies_path, class: "group whitespace <%= tag.span(render("icons/building <%= tag.span("Companies", class: "text <% end %> <%= link_to tasks_path, class: "group whitespace <%= tag.span(render("icons/pencil <%= tag.span("Tasks", class: "text <% end %> <% end %> <%= tag.span(render("icons/home"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Dashboard", class: "text - gray-600 group - hover:text - gray-700") %> <%= tag.span(render("icons/users"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Contacts", class: "text - gray-600 group - hover:text - gray-700") %> <%= tag.span(render("icons/building - office"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Companies", class: "text - gray-600 group - hover:text - gray-700") %> <%= tag.span(render("icons/pencil - square"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Tasks", class: "text - gray-600 group - hover:text - gray-700") %>
  43. <%= tag.nav(class: "flex space - x-2 …") do %> <%=

    link_to root_path, class: "group whitespace - nowrap …" do %> <%= tag.span(render("icons/home"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Dashboard", class: "text - gray-600 group - hover:text - gray-700") %> <% end %> <%= link_to contacts_path, class: "group whitespace - nowrap …" do %> <%= tag.span(render("icons/users"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Contacts", class: "text - gray-600 group - hover:text - gray-700") %> <% end %> <%= link_to companies_path, class: "group whitespace - nowrap …" do %> <%= tag.span(render("icons/building - office"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Companies", class: "text - gray-600 group - hover:text - gray-700") %> <% end %> <%= link_to tasks_path, class: "group whitespace - nowrap …" do %> <%= tag.span(render("icons/pencil - square"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Tasks", class: "text - gray-600 group - hover:text - gray-700") %> <% end %> <% end %>
  44. <%= tag.nav(class: "flex border - b-2 …") do %> <%=

    tag.div(class: "flex - mb-0.5 …") do %> <%= link_to root_path, class: "… #{ current_page?(tab_path) ? "border - b - red-400" : "border - b - gray-400" <%= tag.span(render("icons/home"), class: "text - gray-400 …") %> <%= tag.span("Dashboard", class: "… #{ "font - semibold" if current_page?(tab_path)}") %> <% end %> <%= link_to contacts_path, class: "… #{ current_page?(contacts_path) ? "border - b - red-400" : "border - b - <%= tag.span(render("icons/users"), class: "text - gray-400 …") %> <%= tag.span("Contacts", class: "… #{ "font - semibold" if current_page?(contacts_path)}") %> <% end %> <%= link_to companies_path, class: "… #{ current_page?(companies_path) ? "border - b - red-400" : "border - - <%= tag.span(render("icons/building - office"), class: "text - gray-400 …") %> <%= tag.span("Companies", class: "… #{ "font - semibold" if current_page?(companies_path)}") %> <% end %> <%= link_to tasks_path, class: "… #{ current_page?(tasks_path) ? "border - b - red-400" : "border - b - gray-4 <%= tag.span(render("icons/pencil - square"), class: "text - gray-400 …") %> <%= tag.span("Tasks", class: "… #{ "font - semibold" if current_page?(tasks_path)}") %> <% end %> <% end %> <% end %>
  45. <%= tag.nav(class: "flex border <%= tag.div(class: "flex <%= link_to root_path,

    class: "… <%= tag.span(render("icons/home"), class: "text <%= tag.span("Dashboard", class: "… <% end %> <%= link_to contacts_path, class: "… <%= tag.span(render("icons/users"), class: "text <%= tag.span("Contacts", class: "… <% end %> <%= link_to companies_path, class: "… <%= tag.span(render("icons/building <%= tag.span("Companies", class: "… <% end %> <%= link_to tasks_path, class: "… <%= tag.span(render("icons/pencil <%= tag.span("Tasks", class: "… <% end %> <% end %> <% end %> border - b-2 <%= tag.div(class: "flex - mb-0.5 …") do %> #{ current_page?(tab_path) ? "border - b - red-400" : "border - b - gray-400" #{ "font - semibold" if current_page?(tab_path)} #{ current_page?(contacts_path) ? "border - b - red-400" : "border - b - #{ "font - semibold" if current_page?(contacts_path)} #{ current_page?(companies_path) ? "border - b - red-400" : "border - - #{ "font - semibold" if current_page?(companies_path)}" #{ current_page?(tasks_path) ? "border - b - red-400" : "border - b - gray-4 #{ "font - semibold" if current_page?(tasks_path)}" <% end %>
  46. <%= tag.nav(class: "flex border - b-2 …") do %> <%=

    tag.div(class: "flex - mb-0.5 …") do %> <%= link_to root_path, class: "… #{ current_page?(tab_path) ? "border - b - red-400" : "border - b - gray-400" <%= tag.span(render("icons/home"), class: "text - gray-400 …") %> <%= tag.span("Dashboard", class: "… #{ "font - semibold" if current_page?(tab_path)}") %> <% end %> <%= link_to contacts_path, class: "… #{ current_page?(contacts_path) ? "border - b - red-400" : "border - b - <%= tag.span(render("icons/users"), class: "text - gray-400 …") %> <%= tag.span("Contacts", class: "… #{ "font - semibold" if current_page?(contacts_path)}") %> <% end %> <%= link_to companies_path, class: "… #{ current_page?(companies_path) ? "border - b - red-400" : "border - - <%= tag.span(render("icons/building - office"), class: "text - gray-400 …") %> <%= tag.span("Companies", class: "… #{ "font - semibold" if current_page?(companies_path)}") %> <% end %> <%= link_to tasks_path, class: "… #{ current_page?(tasks_path) ? "border - b - red-400" : "border - b - gray-4 <%= tag.span(render("icons/pencil - square"), class: "text - gray-400 …") %> <%= tag.span("Tasks", class: "… #{ "font - semibold" if current_page?(tasks_path)}") %> <% end %> <% end %> <% end %>
  47. <%= tag.nav(class: "flex border - b-2 …”) do %> <%=

    tag.div(class: "flex - mb-0.5 …”) do %> … <%= link_to contacts_path, class: "… #{ current_page?(contacts_path) ? "border - b - red-400" : "border - b - <%= tag.span(render("icons/users"), class: "text - gray-400 …") %> <%= tag.span("Contacts", class: “… #{ "font - semibold" if current_page?(contacts_path)}") %> <%= tag.span(current_account.contacts, class: "text - xs leading - none …") %> <% end %> <%= link_to companies_path, class: "… #{ current_page?(companies_path) ? "border - b - red-400" : "border - - <%= tag.span(render("icons/building - office"), class: “text - gray-400 …") %> <%= tag.span("Companies", class: "… #{ "font - semibold" if current_page?(companies_path)}") %> <%= tag.span(current_account.companies, class: "text - xs leading - none …") %> <% end %> <%= link_to tasks_path, class: "… #{ current_page?(tasks_path) ? "border - b - red-400" : "border - b - gray-4 <%= tag.span(render("icons/pencil - square"), class: "text - gray-400 …") %> <%= tag.span("Tasks", class: "… #{ "font - semibold" if current_page?(tasks_path)}") %> <%= tag.span(current_account.tasks, class: "text - xs leading - none … #{ current_account.tasks > 15 ? "bg - red-50 text - red-600" : "bg - gray-50 text - gray-600"}") %> <% end %> <% end %> <% end %>
  48. <%= tag.nav(class: "flex border <%= tag.div(class: "flex … <%= link_to

    contacts_path, class: "… <%= tag.span(render("icons/users"), class: "text <%= tag.span("Contacts", class: “… <%= tag.span(current_account.contacts, class: "text <% end %> <%= link_to companies_path, class: "… <%= tag.span(render("icons/building <%= tag.span("Companies", class: "… <%= tag.span(current_account.companies, class: "text <% end %> <%= link_to tasks_path, class: "… <%= tag.span(render("icons/pencil <%= tag.span("Tasks", class: "… <%= tag.span(current_account.tasks, class: "text <% end %> <% end %> <% end %> <%= tag.span(current_account.contacts, class: "text - xs leading - none …") %> <%= tag.span(current_account.companies, class: "text - xs leading - none …") %> <%= tag.span(current_account.tasks, class: "text - xs leading - none … #{ current_account.tasks > 15 ? "bg - red-50 text - red-600" : "bg - gray-50 text - gray-600"}") %>
  49. <%= tag.nav(class: "flex border - b-2 …") do %> <%=

    tag.div(class: "flex - mb-0.5 …") do %> … <%= link_to tasks_path, class: "… #{ current_page?(tasks_path) ? "border - b - red-400" : "border - b - gray-4 <%= tag.span(render("icons/pencil - square"), class: "text - gray-400 …") %> <%= tag.span("Tasks", class: "… #{ "font - semibold" if current_page?(tasks_path)}") %> <%= tag.span(current_account.tasks, class: "text - xs leading - none … #{ current_account.tasks > 15 ? "bg - red-50 text - red-600" : "bg - gray-50 text - gray-600"}") %> <% end %> <% end %> <% end %>
  50. <%= tag.nav(class: "flex border - b-2 …") do %> <%=

    tag.div(class: "flex - mb-0.5 …") do %> … <%= link_to tasks_path, class: "… #{ current_page?(tasks_path) ? "border - b - red-400" : "border - b - gray-4 <%= tag.span(render("icons/pencil - square"), class: "text - gray-400 …") %> <%= tag.span("Tasks", class: "… #{ "font - semibold" if current_page?(tasks_path)}") %> <%= tag.span(current_account.tasks, class: "text - xs leading - none … #{ current_account.tasks > 15 ? "bg - red-50 text - red-600" : "bg - gray-50 text - gray-600"}") %> <% end %> <% if current_account.plan == "pro" %> <%= link_to reports_path, class: "… #{ current_page?(reports_path) ? "border - b - red-400" : "border - b - <%= tag.span(render("icons/chart - bar"), class: "text - gray-400 …") %> <%= tag.span("Reports", class: "… #{ "font - semibold" if current_page?(reports_path)}") %> <% end %> <% else %> <%= tag.span data: { controller: "tooltip", tooltip_content_value: "Reports are only available on t <%= tag.span(render("icons/chart - bar"), class: "text - gray-300") %> <%= tag.span("Reports", class: "text - gray-400") %> <% end %> <% end %> <% end %> <% end %>
  51. <%= tag.nav(class: "flex border <%= tag.div(class: "flex … <%= link_to

    tasks_path, class: "… <%= tag.span(render("icons/pencil <%= tag.span("Tasks", class: "… <%= tag.span(current_account.tasks, class: "text <% end %> <% if current_account.plan <%= link_to reports_path, class: "… <%= tag.span(render("icons/chart <%= tag.span("Reports", class: "… <% end %> <% else %> <%= tag.span data: { controller: "tooltip", tooltip_content_value: "Reports are only available on t <%= tag.span(render("icons/chart <%= tag.span("Reports", class: "text <% end %> <% end %> <% end %> <% end %> <% if current_account.plan == "pro" %> <%= link_to reports_path, class: "… #{ current_page?(reports_path) ? "border - b - red-400" : "border - b - <%= tag.span(render("icons/chart - bar"), class: "text - gray-400 …") %> <%= tag.span("Reports", class: "… #{ "font - semibold" if current_page?(reports_path)}") %> <% end %> <% else %> <%= tag.span data: { controller: "tooltip", tooltip_content_value: "Reports are only available on t <%= tag.span(render("icons/chart - bar"), class: "text - gray-300") %> <%= tag.span("Reports", class: "text - gray-400") %> <% end %> <% end %>
  52. <%= tag.nav(class: "flex border - b-2 …") do %> <%=

    tag.div(class: "flex - mb-0.5 …") do %> … <% if current_account.plan == "pro" %> … <% end %> <% end %> <% end %>
  53. <%= tag.nav(class: "flex border - b-2 …") do %> <%=

    tag.div(class: "flex - mb-0.5 …") do %> … <% if current_account.plan == "pro" %> … <% end %> <% if current_user.admin? %> <%= tag.span data: { controller: "toggle", toggle_toggle_class: "hidden" }, class: "relative" do %> <%= tag.button(class: "h - full … #{ current_page?(settings_path) ? "border - b - red-400" : "border - b - g <%= tag.span(render("icons/cog-6-tooth"), class: "text - gray-400 …") %> <%= tag.span("Settings", class: "… #{ "font - semibold" if current_page?(settings_path)}") %> <%= tag.span(render("icons/chevron - down"), class: "…") %> <% end %> <%= tag.div(class: "hidden z-40 …", data: { toggle_target: "toggleable" }) do %> <%= link_to "Collaborators", settings_collaborators_path, class: "block w - full …" %> <%= link_to "Notifications", settings_notifications_path, class: "block w - full …" %> <% end %> <% end %> <% end %> <% end %> <% end %>
  54. <%= tag.nav(class: "flex border <%= tag.div(class: "flex … <% if

    current_account.plan … <% end %> <% if current_user.admin? %> <%= tag.span data: { controller: "toggle", toggle_toggle_class: "hidden" }, class: "relative" do %> <%= tag.button(class: "h <%= tag.span(render("icons/cog-6-tooth"), class: "text <%= tag.span("Settings", class: "… <%= tag.span(render("icons/chevron <% end %> <%= tag.div(class: "hidden z-40 …", data: { toggle_target: "toggleable" }) do %> <%= link_to "Collaborators", settings_collaborators_path, class: "block w <%= link_to "Notifications", settings_notifications_path, class: "block w <% end %> <% end %> <% end %> <% end %> <% end %> <% if current_user.admin? %> <%= tag.span data: { controller: "toggle", toggle_toggle_class: "hidden" }, class: "relative" do %> <%= tag.button(class: "h - full … #{ current_page?(settings_path) ? "border - b - red-400" : "border - b - g <%= tag.span(render("icons/cog-6-tooth"), class: "text - gray-400 …") %> <%= tag.span("Settings", class: "… #{ "font - semibold" if current_page?(settings_path)}") %> <%= tag.span(render("icons/chevron - down"), class: "…") %> <% end %> <%= tag.div(class: "hidden z-40 …", data: { toggle_target: "toggleable" }) do %> <%= link_to "Collaborators", settings_collaborators_path, class: "block w - full …" %> <%= link_to "Notifications", settings_notifications_path, class: "block w - full …" %> <% end %> <% end %> <% end %>
  55. <%= tag.nav(class: "flex border - b-2 …") do %> <%=

    tag.div(class: "flex - mb-0.5 …") do %> <%= link_to root_path, class: "… #{ current_page?(tab_path) ? "border - b - red-400" : "border - b - gray-400" <%= tag.span(render("icons/home"), class: "text - gray-400 …") %> <%= tag.span("Dashboard", class: "… #{ "font - semibold" if current_page?(tab_path)}") %> <% end %> … <% end %> <% end %>
  56. <%= tag.nav(class: "flex justify - between …") do %> <%=

    tag.div(class: "flex - mb-0.5 …") do %> <%= link_to root_path, class: "… #{ current_page?(tab_path) ? "border - b - red-400" : "border - b - gray-400" <%= tag.span(render("icons/home"), class: "text - gray-400 …") %> <%= tag.span("Dashboard", class: "… #{ "font - semibold" if current_page?(tab_path)}") %> <% end %> … <% end %> <% if current_account.enabled_feature?(:search) %> <%= tag.div(class: "flex - mb-0.5 space - x-2") do %> <%= form_tag searches_path, method: :get do %> <%= text_field_tag :query, "", placeholder: "Search", autocomplete: "off", class: "w - full text - sm <% end %> <% end %> <% end %> <% end %>
  57. <%= tag.nav(class: "flex justify <%= tag.div(class: "flex <%= link_to root_path,

    class: "… <%= tag.span(render("icons/home"), class: "text <%= tag.span("Dashboard", class: "… <% end %> … <% end %> <% if current_account.enabled_feature?(:search) %> <%= tag.div(class: "flex <%= form_tag searches_path, method: :get do %> <%= text_field_tag :query, "", placeholder: "Search", autocomplete: "off", class: "w <% end %> <% end %> <% end %> <% end %> justify - between <% if current_account.enabled_feature?(:search) %> <%= tag.div(class: "flex - mb-0.5 space - x-2") do %> <%= form_tag searches_path, method: :get do %> <%= text_field_tag :query, "", placeholder: "Search", autocomplete: "off", class: "w - full text - sm <% end %> <% end %> <% end %>
  58. <%= tag.nav(class: "flex justify - between space - x-2 items

    - start border - b-2 border - gray-400") do %> <%= tag.div(class: "flex - mb-0.5 space - x-2") do %> <%= link_to root_path, class: "group whitespace - nowrap flex items - center space - x-1 rounded - md rounded - b - none leading - none py-3 px-3 bg - gray-300 hover:bg - gray-400 border - b-2 # { current_page?(tab_path) ? "border - b - red-400" : "border - b - gray-400"}" do %> <%= tag.span(render("icons/home"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Dashboard", class: "text - gray-600 group - hover:text - gray-700 #{ "font - semibold" if current_page?(tab_path)}") %> <% end %> <%= link_to contacts_path, class: "group whitespace - nowrap flex items - center space - x-1 rounded - md rounded - b - none leading - none py-3 px-3 bg - gray-300 hover:bg - gray-400 border - b-2 #{ current_page?(contacts_path) ? "border - b - red-400" : "border - b - gray-400"}" do % <%= tag.span(render("icons/users"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Contacts", class: "text - gray-600 group - hover:text - gray-700 #{ "font - semibold" if current_page?(contacts_path)}") %> <%= tag.span(current_account.contacts, class: "text - xs leading - none p-1 rounded - md bg - gray-50 text - gray-600") %> <% end %> <%= link_to companies_path, class: "group whitespace - nowrap flex items - center space - x-1 rounded - md rounded - b - none leading - none py-3 px-3 bg - gray-300 hover:bg - gray-400 border - b-2 # { current_page?(companies_path) ? "border - b - red-400" : "border - b - gray-400"}" do <%= tag.span(render("icons/building - office"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Companies", class: "text - gray-600 group - hover:text - gray-700 #{ "font - semibold" if current_page?(companies_path)}") %> <%= tag.span(current_account.companies, class: "text - xs leading - none p-1 rounded - md bg - gray-50 text - gray-600") %> <% end %> <%= link_to tasks_path, class: "group whitespace - nowrap flex items - center space - x-1 rounded - md rounded - b - none leading - none py-3 px-3 bg - gray-300 hover:bg - gray-400 border - b-2 #{ current_page?(tasks_path) ? "border - b - red-400" : "border - b - gray-400"}" do %> <%= tag.span(render("icons/pencil - square"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Tasks", class: "text - gray-600 group - hover:text - gray-700 # { "font - semibold" if current_page?(tasks_path)}") %> <%= tag.span(current_account.tasks, class: "text - xs leading - none p-1 rounded - md # { current_account.tasks > 15 ? "bg - red-50 text - red-600" : "bg - gray-50 text - gray-600"}") %> <% end %> <% if current_account.plan = = "pro" %> <%= link_to reports_path, class: "group whitespace - nowrap flex items - center space - x-1 rounded - md rounded - b - none leading - none py-3 px-3 bg - gray-300 hover:bg - gray-400 border - b-2 # { current_page?(reports_path) ? "border - b - red-400" : "border - b - gray-400"}" do % <%= tag.span(render("icons/chart - bar"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Reports", class: "text - gray-600 group - hover:text - gray-700 #{ "font - semibold" if current_page?(reports_path)}") %> <% end %> <% else %> <%= tag.span data: { controller: "tooltip", tooltip_content_value: "Reports are only available on the Pro plan." }, class: "group whitespace - nowrap flex items - center space - x-1 rounded - md rounded - b - none leading - none py-3 px-3 bg - gray-100 border - b-2 border - <%= tag.span(render("icons/chart - bar"), class: "text - gray-300") %> <%= tag.span("Reports", class: "text - gray-400") %> <% end %> <% end %> <% if current_user.admin? %> <%= tag.span data: { controller: "toggle", toggle_toggle_class: "hidden" }, class: "relative" do %> <%= tag.button(class: "h - full group whitespace - nowrap flex items - center space - x-1 rounded - md rounded - b - none leading - none py-3 px-3 bg - gray-300 hover:bg - gray-400 border - b-2 #{ current_page?(settings_path) ? "border - b - red-400" : "border - b - gray-400"}", data <%= tag.span(render("icons/cog-6-tooth"), class: "text - gray-400 group - hover:text - gray-500") %> <%= tag.span("Settings", class: "text - gray-600 group - hover:text - gray-700 # { "font - semibold" if current_page?(settings_path)}") %> <%= tag.span(render("icons/chevron - down"), class: "text - gray-600 group - hover:text - gray-700") %> <% end %> <%= tag.div(class: "hidden z-40 absolute mt-1 w-60 rounded - md border-2 border - gray-300 shadow - lg py-1 bg - gray-200 divide - y divide - gray-200", data: { toggle_target: "toggleable" }) do %> <%= link_to "Collaborators", settings_collaborators_path, class: "block w - full text - left py-1.5 px-3 text - gray-500 hover:text - gray-600 hover:bg - gray-300" %> <%= link_to "Notifications", settings_notifications_path, class: "block w - full text - left py-1.5 px-3 text - gray-500 hover:text - gray-600 hover:bg - gray-300" %> <% end %> <% end %> <% end %> <% end %> <% if current_account.enabled_feature?(:search) %> <%= tag.div(class: "flex - mb-0.5 space - x-2") do %> <%= form_tag searches_path, method: :get do %> <%= text_field_tag :query, "", placeholder: "Search", autocomplete: "off", class: "w - full text - sm border-2 border - gray-300 bg - gray-50 rounded - md shadow - inner placeholder:text - gray-400" %> <% end %> <% end %> <% end %> <% end %>
  59. Variation Tab type, selected state, authorization, etc. Proliferation Team wants

    to use this in another section Accumulation Icon, counter, dropdown, non-tabs, etc. Churn Changed seven times, may not be done
  60. class TabNav :: BarComponent < ApplicationComponent renders_many :tabs renders_one :extra

    private def nav_classes "flex justify - between space - x-2 items - start border - b-2 border - gray-400" end def section_classes "flex - mb-0.5 space - x-2" end end
  61. <%= tag.nav(class: nav_classes) do %> <%= tag.div(class: section_classes) do %>

    <% tabs.each do |tab| %> <%= tab %> <% end %> <% end %> <% if extra? %> <%= tag.div(class: section_classes) do %> <%= extra %> <% end %> <% end %> <% end %>
  62. class TabNav :: BarComponent < ApplicationComponent renders_many :tabs, types: {

    link: TabNav :: LinkTabComponent, disabled: TabNav :: DisabledTabComponent, dropdown: TabNav :: DropdownTabComponent } renders_one :extra private def nav_classes "flex justify - between space - x-2 items - start border - b-2 border - gray-400" end def section_classes "flex - mb-0.5 space - x-2" end end
  63. class TabNav :: BaseTabComponent < ApplicationComponent private attr_reader :text, :icon,

    :selected def text_classes class_names("text - gray-600 group - hover:text - gray-700", "font - semibold": selected) end def icon_svg render(“icons/ #{ icon}") end def icon_classes "text - gray-400 group - hover:text - gray-500" end def tab_classes(*args) class_names(tab_base, * args) end … end
  64. class TabNav :: LinkTabComponent < TabNav :: BaseTabComponent def initialize(link:,

    text:, icon: nil, selected: false, counter: nil, threshold: nil) @link, @text, @icon, @selected, @counter, @threshold = link, text, icon, selected, counter, threshold end private attr_reader :link, :counter, :threshold def tab_classes super(tab_enabled, " #{ tab_selected}": selected, " #{ tab_unselected}": !selected) end def counter_classes class_names("text - xs leading - none p-1 rounded - md", threshold_classes) end def threshold_classes exceeds_threshold? ? "bg - red-50 text - red-600" : "bg - gray-50 text - gray-600" end def exceeds_threshold? return false if counter.blank? || threshold.blank? counter > threshold end end
  65. <%= link_to link, class: tab_classes do %> <%= tag.span(icon_svg, class:

    icon_classes) if icon.present? %> <%= tag.span(text, class: text_classes) %> <%= tag.span(counter, class: counter_classes) if counter.present? %> <% end %>
  66. class TabNav :: DisabledTabComponent < TabNav :: BaseTabComponent def initialize(text:,

    icon: nil, tooltip: nil) @text, @icon, @tooltip = text, icon, tooltip end private attr_reader :tooltip def text_classes "text - gray-400" end def icon_classes "text - gray-300" end def tab_classes super(" #{ tab_disabled}": true, " #{ tab_unselected}": true) end def tab_disabled "bg - gray-100 cursor - not - allowed" end end
  67. <%= tag.span data: { controller: "tooltip", tooltip_content_value: tooltip }, class:

    tab_classes do %> <%= tag.span(icon_svg, class: icon_classes) if icon.present? %> <%= tag.span(text, class: text_classes) %> <% end %>
  68. class TabNav :: DropdownTabComponent < TabNav :: BaseTabComponent renders_many :items,

    "DropdownItemComponent" def initialize(text:, icon: nil, selected: false) @text, @icon, @selected = text, icon, selected end … class DropdownItemComponent < ApplicationComponent def initialize(text:, link:) @text, @link = text, link end def call link_to text, link, class: "block w - full text - left py-1.5 px-3 text - gray-500 hover:text - gray-600 ho end private attr_reader :text, :link end end
  69. <%= tag.span data: { controller: "toggle", toggle_toggle_class: hidden_class }, class:

    wrapper_classes do <%= tag.button(class: tab_classes, data: { action: "click -> toggle#toggle click@window -> toggle#hide page <%= tag.span(icon_svg, class: icon_classes) if icon.present? %> <%= tag.span(text, class: text_classes) %> <%= tag.span(caret_svg, class: caret_classes) %> <% end %> <%= tag.div(class: menu_classes, data: { toggle_target: "toggleable" }) do %> <% items.each do |item| %> <%= item %> <% end %> <% end %> <% end %>
  70. <%= render TabNav : : BarComponent.new do |c| %> <%

    c.with_tab_link(text: "Dashboard", link: root_path, selected: current_page?(tab_path), icon: "home") %> <% c.with_tab_link(text: "Contacts", link: contacts_path, selected: current_page?(contacts_path), icon: "users <% c.with_tab_link(text: "Companies", link: companies_path, selected: current_page?(companies_path), icon: "bu <% c.with_tab_link(text: "Tasks", link: tasks_path, selected: current_page?(tasks_path), icon: "pencil - square" <% if current_account.plan == "pro" %> <% c.with_tab_link(text: "Reports", link: tasks_path, selected: current_page?(reports_path), icon: "chart - ba <% else %> <% c.with_tab_disabled(text: "Reports", icon: "chart - bar", tooltip: "Reports are only available on the Pro p <% end %> <% if current_user.admin? %> <% c.with_tab_dropdown(text: "Settings", selected: current_page?(settings_path), icon: "cog-6-tooth") do |d| <% d.with_item(text: "Collaborators", link: settings_collaborators_path) %> <% d.with_item(text: "Notifications", link: settings_notifications_path) %> <% end %> <% end %> <% if current_account.enabled_feature?(:search) %> <% c.with_extra do %> <%= form_tag searches_path, method: :get do %> <%= text_field_tag :query, "", placeholder: "Search", autocomplete: "off", class: "w - full text - sm border <% end %> <% end %> <% end %> <% end %>
  71. <%= render TabNav <% c.with_tab_link(text: "Dashboard", link: root_path, selected: current_page?(tab_path),

    icon: "home") %> <% c.with_tab_link(text: "Contacts", link: contacts_path, selected: current_page?(contacts_path), icon: "users <% c.with_tab_link(text: "Companies", link: companies_path, selected: current_page?(companies_path), icon: "bu <% c.with_tab_link(text: "Tasks", link: tasks_path, selected: current_page?(tasks_path), icon: "pencil <% if current_account.plan <% c.with_tab_link(text: "Reports", link: tasks_path, selected: current_page?(reports_path), icon: "chart <% else %> <% c.with_tab_disabled(text: "Reports", icon: "chart <% end %> <% if current_user.admin? %> <% c.with_tab_dropdown(text: "Settings", selected: current_page?(settings_path), icon: "cog-6-tooth") do |d| <% d.with_item(text: "Collaborators", link: settings_collaborators_path) %> <% d.with_item(text: "Notifications", link: settings_notifications_path) %> <% end %> <% end %> <% if current_account.enabled_feature?(:search) %> <% c.with_extra do %> <%= form_tag searches_path, method: :get do %> <%= text_field_tag :query, "", placeholder: "Search", autocomplete: "off", class: "w <% end %> <% end %> <% end %> <% end %> <%= render TabNav : : BarComponent.new do |c| %> <% end %>
  72. <%= render TabNav <% c.with_tab_link(text: "Dashboard", link: root_path, selected: current_page?(tab_path),

    icon: "home") %> <% c.with_tab_link(text: "Contacts", link: contacts_path, selected: current_page?(contacts_path), icon: "users <% c.with_tab_link(text: "Companies", link: companies_path, selected: current_page?(companies_path), icon: "bu <% c.with_tab_link(text: "Tasks", link: tasks_path, selected: current_page?(tasks_path), icon: "pencil <% if current_account.plan <% c.with_tab_link(text: "Reports", link: tasks_path, selected: current_page?(reports_path), icon: "chart <% else %> <% c.with_tab_disabled(text: "Reports", icon: "chart <% end %> <% if current_user.admin? %> <% c.with_tab_dropdown(text: "Settings", selected: current_page?(settings_path), icon: "cog-6-tooth") do |d| <% d.with_item(text: "Collaborators", link: settings_collaborators_path) %> <% d.with_item(text: "Notifications", link: settings_notifications_path) %> <% end %> <% end %> <% if current_account.enabled_feature?(:search) %> <% c.with_extra do %> <%= form_tag searches_path, method: :get do %> <%= text_field_tag :query, "", placeholder: "Search", autocomplete: "off", class: "w <% end %> <% end %> <% end %> <% end %> <% c.with_tab_link(text: "Dashboard", link: root_path, selected: current_page?(tab_path), icon: "home") %> <% c.with_tab_link(text: "Contacts", link: contacts_path, selected: current_page?(contacts_path), icon: "users <% c.with_tab_link(text: "Companies", link: companies_path, selected: current_page?(companies_path), icon: "bu <% c.with_tab_link(text: "Tasks", link: tasks_path, selected: current_page?(tasks_path), icon: "pencil - square" <% c.with_tab_link(text: "Reports", link: tasks_path, selected: current_page?(reports_path), icon: "chart - ba <% c.with_tab_disabled(text: "Reports", icon: "chart - bar", tooltip: "Reports are only available on the Pro p <% c.with_tab_dropdown(text: "Settings", selected: current_page?(settings_path), icon: "cog-6-tooth") do |d| <% end %>
  73. <%= render TabNav <% c.with_tab_link(text: "Dashboard", link: root_path, selected: current_page?(tab_path),

    icon: "home") %> <% c.with_tab_link(text: "Contacts", link: contacts_path, selected: current_page?(contacts_path), icon: "users <% c.with_tab_link(text: "Companies", link: companies_path, selected: current_page?(companies_path), icon: "bu <% c.with_tab_link(text: "Tasks", link: tasks_path, selected: current_page?(tasks_path), icon: "pencil <% if current_account.plan <% c.with_tab_link(text: "Reports", link: tasks_path, selected: current_page?(reports_path), icon: "chart <% else %> <% c.with_tab_disabled(text: "Reports", icon: "chart <% end %> <% if current_user.admin? %> <% c.with_tab_dropdown(text: "Settings", selected: current_page?(settings_path), icon: "cog-6-tooth") do |d| <% d.with_item(text: "Collaborators", link: settings_collaborators_path) %> <% d.with_item(text: "Notifications", link: settings_notifications_path) %> <% end %> <% end %> <% if current_account.enabled_feature?(:search) %> <% c.with_extra do %> <%= form_tag searches_path, method: :get do %> <%= text_field_tag :query, "", placeholder: "Search", autocomplete: "off", class: "w <% end %> <% end %> <% end %> <% end %> <% c.with_extra do %> <% end %>
  74. <%= render TabNav <% c.with_tab_link(text: "Dashboard", link: root_path, selected: current_page?(tab_path),

    icon: "home") %> <% c.with_tab_link(text: "Contacts", link: contacts_path, selected: current_page?(contacts_path), icon: "users <% c.with_tab_link(text: "Companies", link: companies_path, selected: current_page?(companies_path), icon: "bu <% c.with_tab_link(text: "Tasks", link: tasks_path, selected: current_page?(tasks_path), icon: "pencil <% if current_account.plan <% c.with_tab_link(text: "Reports", link: tasks_path, selected: current_page?(reports_path), icon: "chart <% else %> <% c.with_tab_disabled(text: "Reports", icon: "chart <% end %> <% if current_user.admin? %> <% c.with_tab_dropdown(text: "Settings", selected: current_page?(settings_path), icon: "cog-6-tooth") do |d| <% d.with_item(text: "Collaborators", link: settings_collaborators_path) %> <% d.with_item(text: "Notifications", link: settings_notifications_path) %> <% end %> <% end %> <% if current_account.enabled_feature?(:search) %> <% c.with_extra do %> <%= form_tag searches_path, method: :get do %> <%= text_field_tag :query, "", placeholder: "Search", autocomplete: "off", class: "w <% end %> <% end %> <% end %> <% end %> current_page?(tab_path) current_page?(contacts_path) current_page?(companies_path) current_page?(tasks_path) current_account.plan == "pro" current_page?(reports_path) current_user.admin? current_page?(settings_path) current_account.enabled_feature?(:search)
  75. class TabNav :: DropdownTabComponent < TabNav :: BaseTabComponent renders_many :items,

    "DropdownItemComponent" def initialize(text:, icon: nil, selected: false) @text, @icon, @selected = text, icon, selected end … class DropdownItemComponent < ApplicationComponent def initialize(text:, link:) @text, @link = text, link end def call link_to text, link, class: "block w - full text - left py-1.5 px-3 text - gray-500 hover:text - gray-600 ho end private attr_reader :text, :link end end
  76. <%= tag.span data: { controller: "toggle", toggle_toggle_class: hidden_class }, class:

    wrapper_classes do <%= tag.button(class: tab_classes, data: { action: "click -> toggle#toggle click@window -> toggle#hide page <%= tag.span(icon_svg, class: icon_classes) if icon.present? %> <%= tag.span(text, class: text_classes) %> <%= tag.span(caret_svg, class: caret_classes) %> <% end %> <%= tag.div(class: menu_classes, data: { toggle_target: "toggleable" }) do %> <% items.each do |item| %> <%= item %> <% end %> <% end %> <% end %>
  77. app/views/tab_nav/_dropdown_tab.html.erb <%# locals: (text:, icon: nil, selected: false) -%> <%=

    tag.span data: { controller: "toggle", toggle_toggle_class: tab_nav_dropdown_hidden_class }, class: t <%= tag.button(class: tab_nav_tab_classes(dropdown: true, selected: selected), data: { action: "click -> <%= tag.span(tab_nav_icon_svg(icon), class: tab_nav_icon_classes) if icon.present? %> <%= tag.span(text, class: tab_nav_text_classes(selected: selected)) %> <%= tag.span(tab_nav_dropdown_caret_svg, class: tab_nav_dropdown_caret_classes) %> <% end %> <%= tag.div(class: tab_nav_dropdown_menu_classes, data: { toggle_target: "toggleable" }) do %> <%= yield %> <% end %> <% end %>
  78. <%= render "tab_nav/bar", tabs: capture { %> <%= render "tab_nav/link_tab",

    text: "Dashboard", link: root_path, selected: current_page?(tab_path), i … <% }, extra: capture { %> <% if current_account.enabled_feature?(:search) %> <%= form_tag searches_path, method: :get do %> <%= text_field_tag :query, "", placeholder: "Search", autocomplete: "off", class: "w - full text - sm b <% end %> <% end %> <% } %>
  79. module TabNavHelper def tab_nav_text_classes(selected: false, disabled: false) class_names( "text -

    gray-600 group - hover:text - gray-700": !disabled, "font - semibold": selected, "text - gray-400": disabled ) end def tab_nav_icon_svg(icon) render("icons/ #{ icon}") end def tab_nav_icon_classes(disabled: false) disabled ? "text - gray-300" : "text - gray-400 group - hover:text - gray-500" end def tab_nav_tab_classes(selected: false, disabled: false, dropdown: false) class_names( "group whitespace - nowrap flex items - center space - x-1 rounded - md rounded - b - none leading - none py-3 px-3 border - b-2", "h - full": dropdown, "bg - gray-300 hover:bg - gray-400": !disabled, "bg - gray-100 cursor - not - allowed": disabled, "border - b - red-400": selected, "border - b - gray-400": !selected ) end def tab_nav_dropdown_wrapper_classes "relative" end def tab_nav_dropdown_caret_svg render("icons/chevron - down") end def tab_nav_dropdown_caret_classes "text - gray-600 group - hover:text - gray-700" end def tab_nav_dropdown_menu_classes class_names( tab_nav_dropdown_hidden_class, "z-40 absolute mt-1 w-60 rounded - md border-2 border - gray-300 shadow - lg py-1 bg - gray-200 divide - y divide - gray-200" ) end def tab_nav_dropdown_hidden_class "hidden" end def tab_nav_dropdown_item_classes "block w - full text - left py-1.5 px-3 text - gray-500 hover:text - gray-600 hover:bg - gray-300" end end
  80. Summary 1. Implement designs in traditional templates 2. Watch for

    volatility (high churn & complexity) 3. Extract view components and regain stability
  81. require "test_helper" class TabNav :: BarComponentTest < ViewComponent :: TestCase

    def test_render_component render_inline(TabNav :: BarComponent.new) do |c| c.with_tab_link(text: "Home", link: "/home") c.with_tab_link(text: "Reports", link: "/reports", icon: "chart - bar") c.with_extra { "<strong>Something else. </ strong>".html_safe } end assert_selector("nav") assert_selector("a", count: 2) assert_link("Home", href: "/home") assert_link("Reports", href: "/reports") assert_selector("svg", count: 1) assert_selector("strong", text: "Something else.") end end
  82. Other Libraries Phlex 
 https://github.com/phlex-ruby/phlex Nice Partials 
 https://github.com/bullet-train-co/ nice_partials

    Komponent 
 https://github.com/komposable/komponent Matestack 
 https://github.com/matestack Cells 
 https://github.com/trailblazer/cells Jumpstart Pro JumpstartComponent
  83. Resources https://viewcomponent.org/ https://github.com/viewcomponent/view_component https://github.com/palkan/view_component-contrib https://github.com/pantographe/view_component-form https://github.com/phlex-ruby/phlex https://github.com/bullet-train-co/nice_partials https://github.com/trailblazer/cells https://github.com/komposable/komponent https://github.com/matestack

    https://evilmartians.com/chronicles/viewcomponent-in-the-wild-building-modern-rails-frontends https://evilmartians.com/chronicles/viewcomponent-in-the-wild-supercharging-your-components https://evilmartians.com/chronicles/viewcomponent-in-the-wild-embracing-tailwindcss-classes-and-html-attributes https://railsnotes.xyz/blog/rails-viewcomponent-tips https://dev.to/nejremeslnici/from-partials-to-viewcomponents-writing-reusable-front-end-code-in-rails-1c9o https://www.honeybadger.io/blog/rails-viewcomponent/ https://thoughtbot.com/blog/hotwire-turbo-streaming-viewcomponents https://tips.rstankov.com/p/tips-for-using-viewcomponents-in https://viewcomponent.org/viewcomponents-at-github.html https://www.codewithjason.com/the-problem-that-viewcomponent-solves-for-me/ https://evilmartians.com/chronicles/evil-front-part-1#block-mentality https://bigmedium.com/ideas/design-system-pace-layers-slow-fast.html https://github.com/whitesmith/rubycritic/blob/main/docs/core-metrics.md#churn-and-complexity https://www.railsinside.com/tutorials/487-how-to-score-your-rails-apps-complexity-before-refactoring.html https://www.youtube.com/watch?v=ar8RMbDPoSY Slides: https://speakerdeck.com/rstankov/component-driven-ui-with-viewcomponent-gem https://www.youtube.com/watch?v=sIxvxp7E0xg Slides: https://speakerdeck.com/palkan/railsconf-2021-frontendless-rails-frontend https://www.youtube.com/watch?v=YVYRus_2KZM https://www.youtube.com/watch?v=QoetqsBCsbE