mcpField
R
rails-components

rails-components

github.com/thinkoodle设计与 UI7175 次安装8 周前更新
代码仓库

关于 rails-components

rails-components 是一项指导开发者使用 CSS 类、partials、helpers 和 ViewComponent 构建可复用 Rails 界面组件的 Skill。它帮助选择合适且尽量简单的实现方式,并统一组件结构与约定。

主要功能

  1. 01从 CSS 到 ViewComponent 的选择指南
  2. 02可复用 partial 与 helper 模式
  3. 03组件命名和文件约定
  4. 04插槽与变体实现指导
  5. 05组件展示页设置

适用场景

  1. 01为新界面组件选择实现方式
  2. 02重构重复的 Rails 标记
  3. 03整理共享组件样式与模板
  4. 04创建开发用组件展示页

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/thinkoodle/rails-skills --skill rails-components

安装后在 Claude Code 中输入/rails-components确认已加载。

附带文件reference.md

详细介绍

Rails 组件模式

使用 Rails 基础构件构建可复用、自包含、可组合的 UI 组件——局部模板、CSS 类和辅助方法。

理念

核心原则:

  1. 从 CSS 类开始——能用类解决,就不要急着使用局部模板
  2. 自包含——每个组件都在一处管理自己的标记和样式
  3. 可组合——组件可以自然地嵌套和组合
  4. 一致——相同的组件 = 相同的标记 = 各处相同的外观
  5. 循序渐进——只有在遇到问题时,才从 CSS 升级到局部模板,再到辅助方法

组件阶梯:

ViewComponent (gem)     ← Complex: encapsulated Ruby + template + tests
Helper method           ← Frequent: shorthand for common partials
Partial with locals     ← Structured: logic, slots, complex markup
CSS classes only        ← Simple: single element, few variants

始终从最底层开始。只有遇到问题时才向上升级。

何时使用此技能

  • 构建新的可复用 UI 组件(按钮、卡片、模态框等)
  • 在 CSS 类、局部模板、辅助方法或 ViewComponent 之间做选择
  • 创建空状态、警报、徽章或数据表格
  • 将重复的标记重构为共享组件
  • 搭建用于开发的组件库
  • 实现变体模式(尺寸、颜色、状态)
  • 组织组件文件和样式

决策框架

使用 CSS 类的情况:
  • 组件是单个元素或简单的包装元素(<button>、<span>、<div>)
  • 变体仅影响视觉效果(颜色、尺寸、间距)
  • 不需要条件逻辑
  • 除内部文本外,不需要内容插槽

示例: 按钮、徽章、简单卡片、表单输入框、分隔线

使用局部模板的情况:
  • 组件包含条件区域(根据参数显示或隐藏)
  • 包含多个命名内容区域(页眉、正文、页脚)
  • 标记结构并不简单(5 个以上元素)
  • 需要默认值或计算后的属性

示例: 空状态、模态框、复杂卡片、数据表格、带图标的警报

使用辅助方法的情况:
  • 局部模板的调用频率很高(10 个以上调用处)
  • 希望使用更简洁的 Ruby API:ui_badge("Active", variant: :success)
  • ERB 渲染语法显得繁琐的简单组件
  • 希望在 Ruby 中组合多个元素

示例: 徽章、状态指示器、图标按钮、面包屑导航

使用 ViewComponent 的情况:
  • 组件需要自己的单元测试
  • 复杂的状态逻辑不适合放在视图中
  • 团队规模较大,组件需要严格的接口
  • 正在构建包含数十个组件的设计系统

大多数 Rails 应用都不需要 ViewComponent。局部模板和辅助方法足以满足 90% 的需求。

说明

第 1 步:检查现有组件

务必先查找现有模式:

# Find existing component partials
find app/views -path "*/components/*" -o -path "*/_component*" | head -20

# Find component CSS
find app/assets -name "*component*" -o -path "*/components/*" | head -20

# Find component helpers
rg "def ui_|def component_" app/helpers/

# Check for ViewComponent usage
ls app/components/ 2>/dev/null
rg "ViewComponent" Gemfile

遵循项目现有约定。 一致性优先于个人偏好。

第 2 步:选择合适的方案

参考上面的决策框架。请自问:

  1. 只用 CSS 类就能实现吗?→ 那就这样做。
  2. 需要条件逻辑或插槽吗?→ 使用局部模板。
  3. 是否有 10 个以上位置以相同模式调用?→ 添加辅助方法。
  4. 是否需要隔离测试和复杂状态?→ 使用 ViewComponent。
第 3 步:文件组织
app/views/components/          # Shared component partials
├── _modal.html.erb
├── _empty_state.html.erb
├── _card.html.erb
├── _alert.html.erb
└── _data_table.html.erb

app/assets/stylesheets/components/   # One CSS file per component
├── buttons.css
├── badges.css
├── cards.css
├── modals.css
├── empty-states.css
├── alerts.css
└── tables.css

app/helpers/
└── component_helper.rb        # Helper shortcuts for common components

命名约定:

  • 局部模板:app/views/components/ 中的 _snake_case.html.erb
  • CSS 文件:app/assets/stylesheets/components/ 中的 kebab-case.css
  • 辅助方法:在 ComponentHelper 中使用 ui_component_name 前缀
  • CSS 类:.component-name、.component-name-element、.component-name--variant
第 4 步:构建纯 CSS 组件

对于简单组件,定义 CSS 类并直接在模板中使用:

/* app/assets/stylesheets/components/buttons.css */
@layer components {
  .btn {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 1rem;
    border-radius: 0.375rem;
    font-weight: 500;
    cursor: pointer;
    transition: background-color 0.15s;
  }

  .btn-primary { background: var(--color-primary); color: white; }
  .btn-secondary { background: var(--color-surface); border: 1px solid var(--color-border); }
  .btn-danger { background: var(--color-danger); color: white; }
  .btn-ghost { background: transparent; }
  .btn-sm { padding: 0.25rem 0.75rem; font-size: 0.875rem; }
  .btn-lg { padding: 0.75rem 1.5rem; }
  .btn-icon { padding: 0.5rem; }
}
<%# Usage — no partial needed %>
<button class="btn btn-primary">Save</button>
<button class="btn btn-danger btn-sm">Delete</button>
<%= link_to "View", post_path(@post), class: "btn btn-secondary" %>
第 5 步:构建基于局部模板的组件

每个局部模板都必须包含注释块,说明其接口:

<%# app/views/components/_empty_state.html.erb %>
<%#
  Empty State Component

  Arguments:
    icon:        (String) Icon name — required
    title:       (String) Main heading — required
    description: (String, optional) Supporting text
    class:       (String, optional) Additional CSS classes

  Block: Optional action buttons
%>
<div class="empty-state <%= local_assigns[:class] %>">
  <div class="empty-state-icon">
    <%= lucide_icon icon, size: 48 %>
  </div>

  <h3 class="empty-state-title"><%= title %></h3>

  <% if local_assigns[:description] %>
    <p class="empty-state-description"><%= description %></p>
  <% end %>

  <% if block_given? %>
    <div class="empty-state-actions">
      <%= yield %>
    </div>
  <% end %>
</div>

局部模板的关键规则:

  1. 使用 local_assigns[:key] 检查可选参数——不要使用 defined? 或 if key
  2. 当参数需要后备值时,使用 ||= 设置默认值
  3. 使用 block_given? 检查是否传入了代码块
  4. 使用 yield 提供主要内容插槽
  5. 使用 capture 提供其他命名插槽
第 6 步:使用 Capture 实现插槽

对于包含多个内容区域的组件:

<%# app/views/components/_card.html.erb %>
<%#
  Card Component

  Arguments:
    header:  (String|HTML, optional) Header content — use capture for complex HTML
    footer:  (String|HTML, optional) Footer content — use capture for complex HTML
    class:   (String, optional) Additional CSS classes
    variant: (Symbol, optional) Card variant — :default, :stat, :interactive

  Block: Card body content (required)
%>
<%
  variant ||= :default
  css_class = ["card", ("card-#{variant}" unless variant == :default), local_assigns[:class]].compact.join(" ")
%>
<div class="<%= css_class %>">
  <% if local_assigns[:header] %>
    <div class="card-header"><%= header %></div>
  <% end %>

  <div class="card-body">
    <%= yield %>
  </div>

  <% if local_assigns[:footer] %>
    <div class="card-footer"><%= footer %></div>
  <% end %>
</div>

调用方使用 capture 提供富内容插槽:

<%= render "components/card",
    header: capture { tag.h3("Settings", class: "card-title") },
    footer: capture {
      link_to("Cancel", "#", class: "btn btn-secondary") +
      link_to("Save", "#", class: "btn btn-primary")
    } do %>
  <p>Card body content here.</p>
<% end %>
第 7 步:添加辅助快捷方式

对于经常使用的组件,创建辅助方法:

# app/helpers/component_helper.rb
module ComponentHelper
  def ui_badge(text, variant: :muted)
    tag.span(text, class: "badge badge-#{variant}")
  end

  def ui_empty_state(title:, icon: nil, description: nil, &block)
    render("components/empty_state",
      title: title,
      icon: icon,
      description: description,
      &block)
  end

  def ui_alert(variant: :info, dismissible: false, &block)
    render("components/alert",
      variant: variant,
      dismissible: dismissible,
      &block)
  end
end

使用方式会变得简洁:

<%= ui_badge "Active", variant: :success %>
<%= ui_badge "Draft" %>

<%= ui_empty_state(
  icon: "inbox",
  title: "No messages",
  description: "Check back later") %>
第 8 步:处理变体

纯视觉改动使用 CSS 变体:

/* Size variants */
.btn-sm { padding: 0.25rem 0.75rem; font-size: 0.875rem; }
.btn-lg { padding: 0.75rem 1.5rem; }

/* State variants */
.card-interactive:hover { border-color: var(--color-border-strong); }
.card-flush .card-body { padding: 0; }

标记发生变化时使用 部分参数变体:

<%# In the partial %>
<%
  variant ||= :info
  icon_name = { success: "check-circle", error: "alert-circle",
                warning: "alert-triangle", info: "info" }[variant.to_sym]
%>
<div class="alert alert-<%= variant %>" role="alert">
  <%= lucide_icon icon_name, size: 18 %>
  <span class="alert-content"><%= yield %></span>
</div>
第 9 步:创建组件展示页

这是开发的必备项。 让任何人都能浏览所有组件:

# config/routes.rb
if Rails.env.development?
  get "components", to: "components#index"
end
# app/controllers/components_controller.rb
class ComponentsController < ApplicationController
  layout "application"

  def index
  end
end
<%# app/views/components/index.html.erb %>
<div style="max-width: 48rem; margin: 2rem auto; padding: 0 1rem;">
  <h1>Component Gallery</h1>

  <h2>Buttons</h2>
  <div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 2rem;">
    <button class="btn btn-primary">Primary</button>
    <button class="btn btn-secondary">Secondary</button>
    <button class="btn btn-danger">Danger</button>
    <button class="btn btn-ghost">Ghost</button>
    <button class="btn btn-sm">Small</button>
  </div>

  <h2>Badges</h2>
  <div style="display: flex; gap: 0.5rem; margin-bottom: 2rem;">
    <span class="badge badge-muted">Draft</span>
    <span class="badge badge-success">Active</span>
    <span class="badge badge-warning">Pending</span>
    <span class="badge badge-danger">Failed</span>
  </div>

  <h2>Empty States</h2>
  <%= render "components/empty_state",
      icon: "inbox",
      title: "No messages yet",
      description: "When you receive messages, they'll appear here." do %>
    <button class="btn btn-primary">Compose</button>
  <% end %>

  <h2>Alerts</h2>
  <%= render "components/alert", variant: :info do %>Info message<% end %>
  <%= render "components/alert", variant: :success do %>Success message<% end %>
  <%= render "components/alert", variant: :warning do %>Warning message<% end %>
  <%= render "components/alert", variant: :error do %>Error message<% end %>
</div>

反模式

  1. 所有内容都用 partial — <span class="badge"> 不需要 render "components/badge"。使用 CSS 类就够了。
  2. 使用 defined?(variable) — 在 partial 中始终使用 local_assigns[:variable]。defined? 并不可靠。
  3. 将全局样式用于组件专属内容 — 组件 CSS 应放在 components/ 中。不要污染 application.css。
  4. 命名不一致 — 选择一种约定并坚持使用。使用 _empty_state.html.erb,而不是 _emptyState.html.erb。
  5. 万能组件 — 如果一个 partial 接收 10 个以上的参数,就将其拆分为更小的组件。
  6. 缺少文档 — 每个 partial 都需要一个列出其参数的注释块。
  7. 将 HTML 字符串作为参数传递 — 使用 capture { } 块或 yield,绝不要传入原始 HTML 字符串。
  8. 没有展示页 — 如果无法在同一处查看所有组件,它们就会逐渐变得不一致。

CSS 约定

/* Use @layer for specificity management */
@layer components {
  .component-name { /* base styles */ }
  .component-name-element { /* child element */ }
  .component-name--variant { /* BEM variant — OR use component-variant */ }
}

设计令牌的使用:

.component {
  color: var(--color-ink);           /* Text colors */
  background: var(--color-surface);  /* Backgrounds */
  border: 1px solid var(--color-border);  /* Borders */
  padding: var(--space-4);           /* Spacing scale */
  font-size: var(--text-sm);         /* Type scale */
  border-radius: var(--radius-md);   /* Border radius */
}

如果项目使用设计令牌/CSS 自定义属性,就使用它们。如果没有,就使用一致的原始值。

快速参考

local_assigns 速查表
<%# Check if argument was passed (even if nil) %>
<% if local_assigns.key?(:title) %>

<%# Check if argument was passed AND is truthy %>
<% if local_assigns[:title] %>

<%# Default value %>
<% variant = local_assigns[:variant] || :default %>
<%# OR %>
<% variant ||= :default %>

<%# Pass-through CSS classes %>
<div class="component <%= local_assigns[:class] %>">
Render 语法
<%# Simple — no block %>
<%= render "components/empty_state", icon: "inbox", title: "Empty" %>

<%# With block %>
<%= render "components/modal", title: "Confirm" do %>
  <p>Are you sure?</p>
<% end %>

<%# With capture slots %>
<%= render "components/card",
    header: capture { tag.h3("Title") },
    footer: capture { tag.button("Save", class: "btn btn-primary") } do %>
  <p>Body</p>
<% end %>
常见组件模板

完整模板请参见此技能目录中的 reference.md:

  • 按钮、徽章(仅 CSS)
  • 卡片、空状态、提示框(partial)
  • 使用 Stimulus 的模态框(partial + JS)
  • 数据表格(partial)
  • 上述所有组件的辅助方法

评论

支持 Markdown