agent-notes 2.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_notes/VERSION +1 -0
- agent_notes/__init__.py +1 -0
- agent_notes/__main__.py +4 -0
- agent_notes/cli.py +348 -0
- agent_notes/commands/__init__.py +27 -0
- agent_notes/commands/_install_helpers.py +262 -0
- agent_notes/commands/build.py +170 -0
- agent_notes/commands/doctor.py +112 -0
- agent_notes/commands/info.py +95 -0
- agent_notes/commands/install.py +99 -0
- agent_notes/commands/list.py +169 -0
- agent_notes/commands/memory.py +430 -0
- agent_notes/commands/regenerate.py +152 -0
- agent_notes/commands/set_role.py +143 -0
- agent_notes/commands/uninstall.py +26 -0
- agent_notes/commands/update.py +169 -0
- agent_notes/commands/validate.py +199 -0
- agent_notes/commands/wizard.py +720 -0
- agent_notes/config.py +154 -0
- agent_notes/data/agents/agents.yaml +352 -0
- agent_notes/data/agents/analyst.md +45 -0
- agent_notes/data/agents/api-reviewer.md +47 -0
- agent_notes/data/agents/architect.md +46 -0
- agent_notes/data/agents/coder.md +28 -0
- agent_notes/data/agents/database-specialist.md +45 -0
- agent_notes/data/agents/debugger.md +47 -0
- agent_notes/data/agents/devil.md +47 -0
- agent_notes/data/agents/devops.md +38 -0
- agent_notes/data/agents/explorer.md +23 -0
- agent_notes/data/agents/integrations.md +44 -0
- agent_notes/data/agents/lead.md +216 -0
- agent_notes/data/agents/performance-profiler.md +44 -0
- agent_notes/data/agents/refactorer.md +48 -0
- agent_notes/data/agents/reviewer.md +44 -0
- agent_notes/data/agents/security-auditor.md +44 -0
- agent_notes/data/agents/system-auditor.md +38 -0
- agent_notes/data/agents/tech-writer.md +32 -0
- agent_notes/data/agents/test-runner.md +36 -0
- agent_notes/data/agents/test-writer.md +39 -0
- agent_notes/data/cli/claude.yaml +25 -0
- agent_notes/data/cli/copilot.yaml +18 -0
- agent_notes/data/cli/opencode.yaml +22 -0
- agent_notes/data/commands/brainstorm.md +8 -0
- agent_notes/data/commands/debug.md +9 -0
- agent_notes/data/commands/review.md +10 -0
- agent_notes/data/global-claude.md +290 -0
- agent_notes/data/global-copilot.md +27 -0
- agent_notes/data/global-opencode.md +40 -0
- agent_notes/data/hooks/session-context.md.tpl +19 -0
- agent_notes/data/models/claude-haiku-4-5.yaml +15 -0
- agent_notes/data/models/claude-opus-4-1.yaml +16 -0
- agent_notes/data/models/claude-opus-4-5.yaml +16 -0
- agent_notes/data/models/claude-opus-4-6.yaml +16 -0
- agent_notes/data/models/claude-opus-4-7.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4-5.yaml +16 -0
- agent_notes/data/models/claude-sonnet-4-6.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4.yaml +16 -0
- agent_notes/data/pricing.yaml +33 -0
- agent_notes/data/roles/orchestrator.yaml +5 -0
- agent_notes/data/roles/reasoner.yaml +5 -0
- agent_notes/data/roles/scout.yaml +5 -0
- agent_notes/data/roles/worker.yaml +5 -0
- agent_notes/data/rules/code-quality.md +9 -0
- agent_notes/data/rules/safety.md +10 -0
- agent_notes/data/scripts/cost-report +211 -0
- agent_notes/data/skills/brainstorming/SKILL.md +57 -0
- agent_notes/data/skills/code-review/SKILL.md +64 -0
- agent_notes/data/skills/debugging-protocol/SKILL.md +51 -0
- agent_notes/data/skills/docker-compose/SKILL.md +318 -0
- agent_notes/data/skills/docker-compose-advanced/SKILL.md +575 -0
- agent_notes/data/skills/docker-dockerfile/SKILL.md +385 -0
- agent_notes/data/skills/docker-dockerfile-languages/SKILL.md +293 -0
- agent_notes/data/skills/git/SKILL.md +87 -0
- agent_notes/data/skills/rails-active-storage/SKILL.md +321 -0
- agent_notes/data/skills/rails-broadcasting/SKILL.md +374 -0
- agent_notes/data/skills/rails-concerns/SKILL.md +806 -0
- agent_notes/data/skills/rails-controllers/SKILL.md +510 -0
- agent_notes/data/skills/rails-controllers-advanced/SKILL.md +441 -0
- agent_notes/data/skills/rails-helpers/SKILL.md +677 -0
- agent_notes/data/skills/rails-initializers/SKILL.md +79 -0
- agent_notes/data/skills/rails-javascript/SKILL.md +567 -0
- agent_notes/data/skills/rails-jobs/SKILL.md +700 -0
- agent_notes/data/skills/rails-kamal/SKILL.md +483 -0
- agent_notes/data/skills/rails-lib/SKILL.md +101 -0
- agent_notes/data/skills/rails-mailers/SKILL.md +321 -0
- agent_notes/data/skills/rails-migrations/SKILL.md +268 -0
- agent_notes/data/skills/rails-models/SKILL.md +459 -0
- agent_notes/data/skills/rails-models-advanced/SKILL.md +398 -0
- agent_notes/data/skills/rails-routes/SKILL.md +804 -0
- agent_notes/data/skills/rails-style/SKILL.md +538 -0
- agent_notes/data/skills/rails-testing-controllers/SKILL.md +343 -0
- agent_notes/data/skills/rails-testing-models/SKILL.md +296 -0
- agent_notes/data/skills/rails-testing-system/SKILL.md +375 -0
- agent_notes/data/skills/rails-validations/SKILL.md +108 -0
- agent_notes/data/skills/rails-view-components/SKILL.md +511 -0
- agent_notes/data/skills/rails-view-components-advanced/SKILL.md +376 -0
- agent_notes/data/skills/rails-views/SKILL.md +413 -0
- agent_notes/data/skills/rails-views-advanced/SKILL.md +450 -0
- agent_notes/data/skills/refactoring-protocol/SKILL.md +64 -0
- agent_notes/data/skills/tdd/SKILL.md +57 -0
- agent_notes/data/templates/__init__.py +1 -0
- agent_notes/data/templates/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__init__.py +1 -0
- agent_notes/data/templates/frontmatter/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/claude.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/cursor.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/opencode.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/claude.py +44 -0
- agent_notes/data/templates/frontmatter/opencode.py +104 -0
- agent_notes/doctor_checks.py +189 -0
- agent_notes/domain/__init__.py +17 -0
- agent_notes/domain/agent.py +34 -0
- agent_notes/domain/cli_backend.py +40 -0
- agent_notes/domain/diagnostics.py +29 -0
- agent_notes/domain/diff.py +44 -0
- agent_notes/domain/model.py +27 -0
- agent_notes/domain/role.py +13 -0
- agent_notes/domain/rule.py +13 -0
- agent_notes/domain/skill.py +15 -0
- agent_notes/domain/state.py +46 -0
- agent_notes/install_state.py +11 -0
- agent_notes/registries/__init__.py +16 -0
- agent_notes/registries/_base.py +46 -0
- agent_notes/registries/agent_registry.py +107 -0
- agent_notes/registries/cli_registry.py +89 -0
- agent_notes/registries/model_registry.py +85 -0
- agent_notes/registries/role_registry.py +64 -0
- agent_notes/registries/rule_registry.py +80 -0
- agent_notes/registries/skill_registry.py +141 -0
- agent_notes/services/__init__.py +8 -0
- agent_notes/services/diagnostics/__init__.py +47 -0
- agent_notes/services/diagnostics/_checks.py +272 -0
- agent_notes/services/diagnostics/_display.py +346 -0
- agent_notes/services/diagnostics/_fix.py +169 -0
- agent_notes/services/diff.py +349 -0
- agent_notes/services/fs.py +195 -0
- agent_notes/services/install_state_builder.py +210 -0
- agent_notes/services/installer.py +293 -0
- agent_notes/services/memory_backend.py +155 -0
- agent_notes/services/rendering.py +329 -0
- agent_notes/services/session_context.py +23 -0
- agent_notes/services/settings_writer.py +79 -0
- agent_notes/services/state_store.py +249 -0
- agent_notes/services/ui.py +419 -0
- agent_notes/services/user_config.py +62 -0
- agent_notes/services/validation.py +67 -0
- agent_notes/state.py +21 -0
- agent_notes-2.0.4.dist-info/METADATA +14 -0
- agent_notes-2.0.4.dist-info/RECORD +162 -0
- agent_notes-2.0.4.dist-info/WHEEL +5 -0
- agent_notes-2.0.4.dist-info/entry_points.txt +2 -0
- agent_notes-2.0.4.dist-info/licenses/LICENSE +21 -0
- agent_notes-2.0.4.dist-info/top_level.txt +2 -0
- tests/conftest.py +20 -0
- tests/functional/__init__.py +0 -0
- tests/functional/test_build_commands.py +88 -0
- tests/functional/test_registries.py +128 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_build_output.py +129 -0
- tests/plugins/__init__.py +0 -0
- tests/plugins/test_agents.py +93 -0
- tests/plugins/test_skills.py +77 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-view-components
|
|
3
|
+
description: "ViewComponent: reusable components, slots, testing, and previews"
|
|
4
|
+
group: rails
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ViewComponent
|
|
8
|
+
|
|
9
|
+
A framework for building reusable, testable & encapsulated view components in Ruby on Rails.
|
|
10
|
+
|
|
11
|
+
> **Source**: This guide is based on [ViewComponent Official Documentation](https://viewcomponent.org/) and the [ViewComponent GitHub Repository](https://github.com/ViewComponent/view_component).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Philosophy
|
|
16
|
+
|
|
17
|
+
**"ViewComponent is to UI what ActiveRecord is to SQL"** — brings conceptual compression to UI development.
|
|
18
|
+
|
|
19
|
+
ViewComponent was created to manage complexity in GitHub.com's view layer, providing abstraction for common UI patterns to improve quality and consistency. It exposes existing complexity, which aids refactoring and comprehension.
|
|
20
|
+
|
|
21
|
+
**Key Benefits:**
|
|
22
|
+
- **Over 100x faster** than similar controller tests (GitHub codebase)
|
|
23
|
+
- **Reusable** - Build once, use anywhere
|
|
24
|
+
- **Testable** - Unit test with `render_inline`
|
|
25
|
+
- **Encapsulated** - Self-contained logic and templates
|
|
26
|
+
|
|
27
|
+
**Source**: [ViewComponent Overview](https://viewcomponent.org/)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## File Structure
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
app/components/
|
|
35
|
+
├── application_component.rb # Base component
|
|
36
|
+
├── button_component.rb # Component class
|
|
37
|
+
├── button_component.html.erb # Component template
|
|
38
|
+
├── card_component.rb
|
|
39
|
+
├── card_component/
|
|
40
|
+
│ ├── card_component.html.erb # Sidecar template
|
|
41
|
+
│ ├── header_component.rb # Nested component
|
|
42
|
+
│ └── header_component.html.erb
|
|
43
|
+
└── alert/
|
|
44
|
+
├── component.rb # Alternative structure
|
|
45
|
+
└── component.html.erb
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Basic Component Structure
|
|
51
|
+
|
|
52
|
+
### Simple Component
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
# app/components/button_component.rb
|
|
56
|
+
class ButtonComponent < ViewComponent::Base
|
|
57
|
+
def initialize(type: :primary, size: :medium, **html_options)
|
|
58
|
+
@type = type
|
|
59
|
+
@size = size
|
|
60
|
+
@html_options = html_options
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
attr_reader :type, :size, :html_options
|
|
65
|
+
|
|
66
|
+
def button_classes
|
|
67
|
+
[
|
|
68
|
+
"btn",
|
|
69
|
+
"btn-#{type}",
|
|
70
|
+
"btn-#{size}",
|
|
71
|
+
html_options[:class]
|
|
72
|
+
].compact.join(" ")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```erb
|
|
78
|
+
<%# app/components/button_component.html.erb %>
|
|
79
|
+
<button class="<%= button_classes %>" <%= tag.attributes(html_options.except(:class)) %>>
|
|
80
|
+
<%= content %>
|
|
81
|
+
</button>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Usage:**
|
|
85
|
+
```erb
|
|
86
|
+
<%= render ButtonComponent.new(type: :primary, size: :large, data: { action: "click->form#submit" }) do %>
|
|
87
|
+
Submit Form
|
|
88
|
+
<% end %>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Component with Slots
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
# app/components/card_component.rb
|
|
95
|
+
class CardComponent < ViewComponent::Base
|
|
96
|
+
# Single slot (rendered at most once)
|
|
97
|
+
renders_one :header, HeaderComponent
|
|
98
|
+
|
|
99
|
+
# Multiple slots (rendered multiple times)
|
|
100
|
+
renders_many :actions, ActionComponent
|
|
101
|
+
|
|
102
|
+
def initialize(variant: :default)
|
|
103
|
+
@variant = variant
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
attr_reader :variant
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```erb
|
|
112
|
+
<%# app/components/card_component.html.erb %>
|
|
113
|
+
<div class="card card-<%= variant %>">
|
|
114
|
+
<% if header? %>
|
|
115
|
+
<div class="card-header">
|
|
116
|
+
<%= header %>
|
|
117
|
+
</div>
|
|
118
|
+
<% end %>
|
|
119
|
+
|
|
120
|
+
<div class="card-body">
|
|
121
|
+
<%= content %>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<% if actions? %>
|
|
125
|
+
<div class="card-actions">
|
|
126
|
+
<% actions.each do |action| %>
|
|
127
|
+
<%= action %>
|
|
128
|
+
<% end %>
|
|
129
|
+
</div>
|
|
130
|
+
<% end %>
|
|
131
|
+
</div>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Usage:**
|
|
135
|
+
```erb
|
|
136
|
+
<%= render CardComponent.new(variant: :primary) do |card| %>
|
|
137
|
+
<% card.with_header(title: "User Profile") %>
|
|
138
|
+
|
|
139
|
+
<p>This is the card body content.</p>
|
|
140
|
+
|
|
141
|
+
<% card.with_action(label: "Edit", url: edit_user_path(@user)) %>
|
|
142
|
+
<% card.with_action(label: "Delete", url: user_path(@user), method: :delete) %>
|
|
143
|
+
<% end %>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Source**: [ViewComponent Slots Guide](https://viewcomponent.org/guide/slots.html)
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Slot Patterns
|
|
151
|
+
|
|
152
|
+
### renders_one (Single Slot)
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
class AlertComponent < ViewComponent::Base
|
|
156
|
+
# Simple passthrough slot
|
|
157
|
+
renders_one :title
|
|
158
|
+
|
|
159
|
+
# Component slot
|
|
160
|
+
renders_one :icon, IconComponent
|
|
161
|
+
|
|
162
|
+
# Lambda slot
|
|
163
|
+
renders_one :footer, ->(text:, classes: nil) do
|
|
164
|
+
content_tag :div, text, class: classes
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```erb
|
|
170
|
+
<div class="alert">
|
|
171
|
+
<% if icon? %>
|
|
172
|
+
<%= icon %>
|
|
173
|
+
<% end %>
|
|
174
|
+
|
|
175
|
+
<% if title? %>
|
|
176
|
+
<h4><%= title %></h4>
|
|
177
|
+
<% end %>
|
|
178
|
+
|
|
179
|
+
<%= content %>
|
|
180
|
+
|
|
181
|
+
<% if footer? %>
|
|
182
|
+
<%= footer %>
|
|
183
|
+
<% end %>
|
|
184
|
+
</div>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Usage:**
|
|
188
|
+
```erb
|
|
189
|
+
<%= render AlertComponent.new do |alert| %>
|
|
190
|
+
<% alert.with_icon(name: "warning") %>
|
|
191
|
+
<% alert.with_title { "Warning" } %>
|
|
192
|
+
|
|
193
|
+
This is an alert message.
|
|
194
|
+
|
|
195
|
+
<% alert.with_footer(text: "Dismiss", classes: "text-sm") %>
|
|
196
|
+
<% end %>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### renders_many (Multiple Slots)
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
class NavigationComponent < ViewComponent::Base
|
|
203
|
+
# Multiple items
|
|
204
|
+
renders_many :items, NavItemComponent
|
|
205
|
+
|
|
206
|
+
# Or with lambda
|
|
207
|
+
renders_many :links, ->(title:, url:, **options) do
|
|
208
|
+
link_to title, url, options
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
```erb
|
|
214
|
+
<nav>
|
|
215
|
+
<ul>
|
|
216
|
+
<% items.each do |item| %>
|
|
217
|
+
<li><%= item %></li>
|
|
218
|
+
<% end %>
|
|
219
|
+
</ul>
|
|
220
|
+
</nav>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**Usage:**
|
|
224
|
+
```erb
|
|
225
|
+
<%= render NavigationComponent.new do |nav| %>
|
|
226
|
+
<% nav.with_item(title: "Home", url: root_path, current: true) %>
|
|
227
|
+
<% nav.with_item(title: "About", url: about_path) %>
|
|
228
|
+
<% nav.with_item(title: "Contact", url: contact_path) %>
|
|
229
|
+
<% end %>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Polymorphic Slots
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
class ModalComponent < ViewComponent::Base
|
|
236
|
+
renders_one :body, types: {
|
|
237
|
+
text: ->(content:) { content_tag :p, content },
|
|
238
|
+
form: FormComponent,
|
|
239
|
+
custom: ->(&block) { capture(&block) }
|
|
240
|
+
}
|
|
241
|
+
end
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**Usage:**
|
|
245
|
+
```erb
|
|
246
|
+
<%# Text variant %>
|
|
247
|
+
<%= render ModalComponent.new do |modal| %>
|
|
248
|
+
<% modal.with_body_text(content: "Simple text content") %>
|
|
249
|
+
<% end %>
|
|
250
|
+
|
|
251
|
+
<%# Form variant %>
|
|
252
|
+
<%= render ModalComponent.new do |modal| %>
|
|
253
|
+
<% modal.with_body_form(url: users_path) %>
|
|
254
|
+
<% end %>
|
|
255
|
+
|
|
256
|
+
<%# Custom variant %>
|
|
257
|
+
<%= render ModalComponent.new do |modal| %>
|
|
258
|
+
<% modal.with_body_custom do %>
|
|
259
|
+
<div>Custom HTML content</div>
|
|
260
|
+
<% end %>
|
|
261
|
+
<% end %>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Source**: [ViewComponent Slots - Polymorphic Slots](https://viewcomponent.org/guide/slots.html)
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Slot Utilities
|
|
269
|
+
|
|
270
|
+
### Predicate Methods
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
class CardComponent < ViewComponent::Base
|
|
274
|
+
renders_one :header
|
|
275
|
+
renders_many :actions
|
|
276
|
+
end
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
```erb
|
|
280
|
+
<% if header? %>
|
|
281
|
+
<%= header %>
|
|
282
|
+
<% end %>
|
|
283
|
+
|
|
284
|
+
<% if actions? %>
|
|
285
|
+
<% actions.each do |action| %>
|
|
286
|
+
<%= action %>
|
|
287
|
+
<% end %>
|
|
288
|
+
<% end %>
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Default Slots
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
class PanelComponent < ViewComponent::Base
|
|
295
|
+
renders_one :title
|
|
296
|
+
|
|
297
|
+
private
|
|
298
|
+
def default_title
|
|
299
|
+
content_tag :h3, "Default Title"
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
```erb
|
|
305
|
+
<%# Will use default if not provided %>
|
|
306
|
+
<%= title %>
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Collection Rendering
|
|
310
|
+
|
|
311
|
+
```ruby
|
|
312
|
+
class TableComponent < ViewComponent::Base
|
|
313
|
+
renders_many :rows
|
|
314
|
+
end
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Usage:**
|
|
318
|
+
```erb
|
|
319
|
+
<%= render TableComponent.new do |table| %>
|
|
320
|
+
<%# Pass array to plural setter %>
|
|
321
|
+
<% table.with_rows(@users.map { |user| { name: user.name, email: user.email } }) %>
|
|
322
|
+
<% end %>
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Source**: [ViewComponent Slots Guide](https://viewcomponent.org/guide/slots.html)
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Testing
|
|
330
|
+
|
|
331
|
+
### Basic Component Test
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
# test/components/button_component_test.rb
|
|
335
|
+
require "test_helper"
|
|
336
|
+
|
|
337
|
+
class ButtonComponentTest < ViewComponent::TestCase
|
|
338
|
+
def test_renders_button
|
|
339
|
+
render_inline ButtonComponent.new(type: :primary) do
|
|
340
|
+
"Click me"
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
assert_selector "button.btn.btn-primary", text: "Click me"
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def test_renders_with_custom_classes
|
|
347
|
+
render_inline ButtonComponent.new(type: :secondary, class: "custom-class")
|
|
348
|
+
|
|
349
|
+
assert_selector "button.btn.btn-secondary.custom-class"
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def test_renders_with_data_attributes
|
|
353
|
+
render_inline ButtonComponent.new(data: { action: "click->test#run" })
|
|
354
|
+
|
|
355
|
+
assert_selector "button[data-action='click->test#run']"
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Testing with Slots
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
class CardComponentTest < ViewComponent::TestCase
|
|
364
|
+
def test_renders_with_header
|
|
365
|
+
render_inline CardComponent.new do |card|
|
|
366
|
+
card.with_header(title: "Test Card")
|
|
367
|
+
"Card content"
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
assert_selector ".card-header", text: "Test Card"
|
|
371
|
+
assert_selector ".card-body", text: "Card content"
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def test_renders_without_header
|
|
375
|
+
render_inline CardComponent.new do
|
|
376
|
+
"Card content"
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
assert_no_selector ".card-header"
|
|
380
|
+
assert_selector ".card-body", text: "Card content"
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def test_renders_multiple_actions
|
|
384
|
+
render_inline CardComponent.new do |card|
|
|
385
|
+
card.with_action(label: "Edit")
|
|
386
|
+
card.with_action(label: "Delete")
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
assert_selector ".card-actions", count: 1
|
|
390
|
+
assert_text "Edit"
|
|
391
|
+
assert_text "Delete"
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### RSpec Setup
|
|
397
|
+
|
|
398
|
+
```ruby
|
|
399
|
+
# spec/rails_helper.rb
|
|
400
|
+
RSpec.configure do |config|
|
|
401
|
+
config.include ViewComponent::TestHelpers, type: :component
|
|
402
|
+
config.include Capybara::RSpecMatchers, type: :component
|
|
403
|
+
end
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
# spec/components/button_component_spec.rb
|
|
408
|
+
require "rails_helper"
|
|
409
|
+
|
|
410
|
+
RSpec.describe ButtonComponent, type: :component do
|
|
411
|
+
it "renders a primary button" do
|
|
412
|
+
render_inline described_class.new(type: :primary) do
|
|
413
|
+
"Submit"
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
expect(page).to have_css "button.btn.btn-primary", text: "Submit"
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
it "applies custom data attributes" do
|
|
420
|
+
render_inline described_class.new(data: { controller: "form" })
|
|
421
|
+
|
|
422
|
+
expect(page).to have_css "button[data-controller='form']"
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
**Source**: [ViewComponent Testing Guide](https://viewcomponent.org/guide/testing.html)
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## Previews
|
|
432
|
+
|
|
433
|
+
Previews provide a quick way to visualize components in isolation during development.
|
|
434
|
+
|
|
435
|
+
### Creating Previews
|
|
436
|
+
|
|
437
|
+
```ruby
|
|
438
|
+
# test/components/previews/button_component_preview.rb
|
|
439
|
+
class ButtonComponentPreview < ViewComponent::Preview
|
|
440
|
+
# Default preview
|
|
441
|
+
def default
|
|
442
|
+
render ButtonComponent.new(type: :primary) do
|
|
443
|
+
"Default Button"
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Named preview
|
|
448
|
+
def primary
|
|
449
|
+
render ButtonComponent.new(type: :primary) do
|
|
450
|
+
"Primary Button"
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def secondary
|
|
455
|
+
render ButtonComponent.new(type: :secondary) do
|
|
456
|
+
"Secondary Button"
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def large
|
|
461
|
+
render ButtonComponent.new(type: :primary, size: :large) do
|
|
462
|
+
"Large Button"
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# With description
|
|
467
|
+
# @label Danger Button
|
|
468
|
+
# @display bg_color "#fee"
|
|
469
|
+
def danger
|
|
470
|
+
render ButtonComponent.new(type: :danger) do
|
|
471
|
+
"Danger Button"
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Preview with Slots
|
|
478
|
+
|
|
479
|
+
```ruby
|
|
480
|
+
class CardComponentPreview < ViewComponent::Preview
|
|
481
|
+
def with_all_slots
|
|
482
|
+
render CardComponent.new(variant: :primary) do |card|
|
|
483
|
+
card.with_header(title: "Card Title")
|
|
484
|
+
card.with_action(label: "Edit", url: "#")
|
|
485
|
+
card.with_action(label: "Delete", url: "#")
|
|
486
|
+
|
|
487
|
+
"This is the card body content with all slots populated."
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def minimal
|
|
492
|
+
render CardComponent.new do
|
|
493
|
+
"Minimal card with no slots."
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### Configuration
|
|
500
|
+
|
|
501
|
+
```ruby
|
|
502
|
+
# config/application.rb
|
|
503
|
+
config.view_component.preview_paths << "#{Rails.root}/app/components/previews"
|
|
504
|
+
config.view_component.show_previews = Rails.env.development?
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
**Access previews:** Visit `/rails/view_components` in development.
|
|
508
|
+
|
|
509
|
+
**Source**: [ViewComponent Previews Guide](https://viewcomponent.org/guide/previews.html)
|
|
510
|
+
|
|
511
|
+
---
|