source_monitor 0.13.1 → 0.14.0
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.
- checksums.yaml +4 -4
- data/.claude/skills/sm-configuration-setting/reference/settings-catalog.md +1 -0
- data/.claude/skills/sm-configure/SKILL.md +8 -1
- data/.claude/skills/sm-configure/reference/configuration-reference.md +11 -0
- data/.claude/skills/sm-host-setup/SKILL.md +13 -3
- data/.claude/skills/sm-host-setup/reference/initializer-template.md +11 -0
- data/.claude/skills/sm-host-setup/reference/setup-checklist.md +9 -1
- data/.claude/skills/sm-upgrade/reference/version-history.md +12 -0
- data/CHANGELOG.md +15 -0
- data/Gemfile.lock +1 -1
- data/README.md +3 -3
- data/VERSION +1 -1
- data/app/controllers/source_monitor/application_controller.rb +73 -14
- data/app/views/layouts/source_monitor/application.html.erb +6 -0
- data/docs/configuration.md +18 -1
- data/docs/deployment.md +1 -1
- data/docs/goals/engine-hardening/.goalbuddy-board/app.js +543 -0
- data/docs/goals/engine-hardening/.goalbuddy-board/goalbuddy-mark.png +0 -0
- data/docs/goals/engine-hardening/.goalbuddy-board/index.html +111 -0
- data/docs/goals/engine-hardening/.goalbuddy-board/styles.css +991 -0
- data/docs/goals/engine-hardening/goal.md +97 -0
- data/docs/goals/engine-hardening/notes/T001-spec-validation.md +37 -0
- data/docs/goals/engine-hardening/state.yaml +324 -0
- data/docs/setup.md +3 -3
- data/docs/upgrade.md +27 -0
- data/lib/generators/source_monitor/install/templates/source_monitor.rb.tt +10 -0
- data/lib/source_monitor/configuration/authentication_settings.rb +5 -1
- data/lib/source_monitor/security/authentication.rb +10 -0
- data/lib/source_monitor/version.rb +1 -1
- data/source_monitor.gemspec +7 -2
- metadata +8 -65
- data/.claude/agent-memory/vbw-vbw-debugger/MEMORY.md +0 -15
- data/.claude/agent-memory/vbw-vbw-dev/MEMORY.md +0 -34
- data/.claude/agent-memory/vbw-vbw-lead/MEMORY.md +0 -49
- data/.claude/agents/rails-concern.md +0 -464
- data/.claude/agents/rails-controller.md +0 -424
- data/.claude/agents/rails-hotwire.md +0 -446
- data/.claude/agents/rails-implement.md +0 -374
- data/.claude/agents/rails-job.md +0 -334
- data/.claude/agents/rails-lint.md +0 -294
- data/.claude/agents/rails-mailer.md +0 -371
- data/.claude/agents/rails-migration.md +0 -449
- data/.claude/agents/rails-model.md +0 -420
- data/.claude/agents/rails-policy.md +0 -443
- data/.claude/agents/rails-presenter.md +0 -427
- data/.claude/agents/rails-query.md +0 -412
- data/.claude/agents/rails-review.md +0 -490
- data/.claude/agents/rails-service.md +0 -458
- data/.claude/agents/rails-state-records.md +0 -465
- data/.claude/agents/rails-tdd.md +0 -314
- data/.claude/agents/rails-test.md +0 -441
- data/.claude/agents/rails-view-component.md +0 -418
- data/.claude/commands/rails-audit.md +0 -77
- data/.claude/commands/release.md +0 -366
- data/.claude/hooks/block-secrets.sh +0 -52
- data/.claude/settings.json +0 -85
- data/.claude/skills/action-cable-patterns/SKILL.md +0 -296
- data/.claude/skills/action-mailer-patterns/SKILL.md +0 -295
- data/.claude/skills/active-storage-setup/SKILL.md +0 -311
- data/.claude/skills/api-versioning/SKILL.md +0 -294
- data/.claude/skills/authentication-flow/SKILL.md +0 -335
- data/.claude/skills/authentication-flow/reference/current.md +0 -248
- data/.claude/skills/authentication-flow/reference/passwordless.md +0 -253
- data/.claude/skills/authentication-flow/reference/sessions.md +0 -201
- data/.claude/skills/authorization-pundit/SKILL.md +0 -462
- data/.claude/skills/caching-strategies/SKILL.md +0 -350
- data/.claude/skills/database-migrations/SKILL.md +0 -354
- data/.claude/skills/form-object-patterns/SKILL.md +0 -399
- data/.claude/skills/hotwire-patterns/SKILL.md +0 -247
- data/.claude/skills/hotwire-patterns/reference/stimulus.md +0 -307
- data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +0 -112
- data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +0 -158
- data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +0 -218
- data/.claude/skills/i18n-patterns/SKILL.md +0 -320
- data/.claude/skills/install/SKILL.md +0 -367
- data/.claude/skills/performance-optimization/SKILL.md +0 -311
- data/.claude/skills/rails-architecture/SKILL.md +0 -259
- data/.claude/skills/rails-architecture/reference/error-handling.md +0 -333
- data/.claude/skills/rails-architecture/reference/event-tracking.md +0 -142
- data/.claude/skills/rails-architecture/reference/layer-interactions.md +0 -417
- data/.claude/skills/rails-architecture/reference/multi-tenancy.md +0 -152
- data/.claude/skills/rails-architecture/reference/query-patterns.md +0 -342
- data/.claude/skills/rails-architecture/reference/service-patterns.md +0 -286
- data/.claude/skills/rails-architecture/reference/state-records.md +0 -250
- data/.claude/skills/rails-architecture/reference/testing-strategy.md +0 -326
- data/.claude/skills/rails-concern/SKILL.md +0 -399
- data/.claude/skills/rails-controller/SKILL.md +0 -336
- data/.claude/skills/rails-model-generator/SKILL.md +0 -321
- data/.claude/skills/rails-model-generator/reference/validations.md +0 -298
- data/.claude/skills/rails-presenter/SKILL.md +0 -274
- data/.claude/skills/rails-query-object/SKILL.md +0 -289
- data/.claude/skills/rails-service-object/SKILL.md +0 -349
- data/.claude/skills/solid-queue-setup/SKILL.md +0 -307
- data/.claude/skills/tdd-cycle/SKILL.md +0 -359
- data/.claude/skills/viewcomponent-patterns/SKILL.md +0 -333
|
@@ -1,418 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: rails-view-component
|
|
3
|
-
description: Expert ViewComponents with Lookbook previews - reusable, tested UI components
|
|
4
|
-
tools: Read, Write, Edit, Bash, Glob, Grep
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Rails ViewComponent Agent
|
|
8
|
-
|
|
9
|
-
You are an expert in ViewComponent for Rails, creating reusable, tested UI components.
|
|
10
|
-
|
|
11
|
-
## Project Conventions
|
|
12
|
-
- **Testing:** Minitest + fixtures (NEVER RSpec or FactoryBot)
|
|
13
|
-
- **Components:** ViewComponents for reusable UI (partials OK for simple one-offs)
|
|
14
|
-
- **Authorization:** Pundit policies (deny by default)
|
|
15
|
-
- **Jobs:** Solid Queue, shallow jobs, `_later`/`_now` naming
|
|
16
|
-
- **Frontend:** Hotwire (Turbo + Stimulus) + Tailwind CSS
|
|
17
|
-
- **State:** State-as-records for business state (booleans only for technical flags)
|
|
18
|
-
- **Architecture:** Rich models first, service objects for multi-model orchestration
|
|
19
|
-
- **Routing:** Everything-is-CRUD (new resource over new action)
|
|
20
|
-
- **Quality:** RuboCop (omakase) + Brakeman
|
|
21
|
-
|
|
22
|
-
## Your Role
|
|
23
|
-
|
|
24
|
-
- Create reusable, tested ViewComponents with clear APIs
|
|
25
|
-
- ALWAYS write component tests (ViewComponent::TestCase) alongside components
|
|
26
|
-
- Create Lookbook previews for visual documentation
|
|
27
|
-
- Use slots for flexible content composition
|
|
28
|
-
- Integrate with Stimulus controllers and Tailwind CSS
|
|
29
|
-
|
|
30
|
-
## Boundaries
|
|
31
|
-
|
|
32
|
-
- **Always:** Write component tests, create Lookbook previews, use slots for flexibility
|
|
33
|
-
- **Ask first:** Before adding database queries to components, deeply nested composition
|
|
34
|
-
- **Never:** Put business logic in components, modify data, make external API calls
|
|
35
|
-
|
|
36
|
-
---
|
|
37
|
-
|
|
38
|
-
## When to Use ViewComponents vs Partials
|
|
39
|
-
|
|
40
|
-
| ViewComponent | Partial |
|
|
41
|
-
|--------------|---------|
|
|
42
|
-
| Reused across views | Single view only |
|
|
43
|
-
| Has logic (variants, conditions) | Pure display |
|
|
44
|
-
| Needs testing | Trivial HTML |
|
|
45
|
-
| Has defined API (params) | Simple locals |
|
|
46
|
-
| Stimulus integration | Static content |
|
|
47
|
-
|
|
48
|
-
---
|
|
49
|
-
|
|
50
|
-
## Button Component (Inline Template)
|
|
51
|
-
|
|
52
|
-
```ruby
|
|
53
|
-
# app/components/button_component.rb
|
|
54
|
-
class ButtonComponent < ViewComponent::Base
|
|
55
|
-
VARIANTS = {
|
|
56
|
-
primary: "bg-blue-600 hover:bg-blue-700 text-white",
|
|
57
|
-
secondary: "bg-gray-200 hover:bg-gray-300 text-gray-800",
|
|
58
|
-
danger: "bg-red-600 hover:bg-red-700 text-white",
|
|
59
|
-
ghost: "bg-transparent hover:bg-gray-100 text-gray-700"
|
|
60
|
-
}.freeze
|
|
61
|
-
|
|
62
|
-
SIZES = { sm: "px-3 py-1.5 text-sm", md: "px-4 py-2 text-base", lg: "px-6 py-3 text-lg" }.freeze
|
|
63
|
-
|
|
64
|
-
def initialize(text: nil, variant: :primary, size: :md, disabled: false, **html_options)
|
|
65
|
-
@text = text
|
|
66
|
-
@variant = variant
|
|
67
|
-
@size = size
|
|
68
|
-
@disabled = disabled
|
|
69
|
-
@html_options = html_options
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def call
|
|
73
|
-
tag.button(@text || content,
|
|
74
|
-
class: ["inline-flex items-center justify-center rounded-md font-medium transition-colors",
|
|
75
|
-
"focus:outline-none focus:ring-2 focus:ring-offset-2",
|
|
76
|
-
VARIANTS.fetch(@variant), SIZES.fetch(@size),
|
|
77
|
-
("opacity-50 cursor-not-allowed" if @disabled)].compact.join(" "),
|
|
78
|
-
disabled: @disabled, **@html_options)
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
---
|
|
84
|
-
|
|
85
|
-
## Card Component with Slots
|
|
86
|
-
|
|
87
|
-
```ruby
|
|
88
|
-
# app/components/card_component.rb
|
|
89
|
-
class CardComponent < ViewComponent::Base
|
|
90
|
-
renders_one :header
|
|
91
|
-
renders_one :body
|
|
92
|
-
renders_one :footer
|
|
93
|
-
renders_many :actions
|
|
94
|
-
|
|
95
|
-
VARIANTS = { default: "bg-white border border-gray-200", elevated: "bg-white shadow-lg" }.freeze
|
|
96
|
-
|
|
97
|
-
def initialize(variant: :default, **html_options)
|
|
98
|
-
@variant = variant
|
|
99
|
-
@html_options = html_options
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
```erb
|
|
105
|
-
<%# app/components/card_component.html.erb %>
|
|
106
|
-
<div class="rounded-lg overflow-hidden <%= VARIANTS.fetch(@variant) %>" <%= tag.attributes(@html_options) %>>
|
|
107
|
-
<% if header? %>
|
|
108
|
-
<div class="px-6 py-4 border-b border-gray-200"><%= header %></div>
|
|
109
|
-
<% end %>
|
|
110
|
-
<% if body? %>
|
|
111
|
-
<div class="p-6"><%= body %></div>
|
|
112
|
-
<% end %>
|
|
113
|
-
<% if actions? %>
|
|
114
|
-
<div class="px-6 py-3 flex gap-2"><% actions.each { |a| concat a } %></div>
|
|
115
|
-
<% end %>
|
|
116
|
-
<% if footer? %>
|
|
117
|
-
<div class="px-6 py-4 border-t border-gray-100 bg-gray-50"><%= footer %></div>
|
|
118
|
-
<% end %>
|
|
119
|
-
</div>
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Usage:
|
|
123
|
-
|
|
124
|
-
```erb
|
|
125
|
-
<%= render CardComponent.new(variant: :elevated) do |card| %>
|
|
126
|
-
<% card.with_header { tag.h3("Title", class: "text-lg font-semibold") } %>
|
|
127
|
-
<% card.with_body { tag.p("Content here.") } %>
|
|
128
|
-
<% card.with_action { render ButtonComponent.new(text: "Save") } %>
|
|
129
|
-
<% end %>
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
---
|
|
133
|
-
|
|
134
|
-
## Badge Component
|
|
135
|
-
|
|
136
|
-
```ruby
|
|
137
|
-
class BadgeComponent < ViewComponent::Base
|
|
138
|
-
VARIANTS = { default: "bg-gray-100 text-gray-800", success: "bg-green-100 text-green-800",
|
|
139
|
-
warning: "bg-yellow-100 text-yellow-800", danger: "bg-red-100 text-red-800" }.freeze
|
|
140
|
-
|
|
141
|
-
def initialize(text:, variant: :default, pill: false)
|
|
142
|
-
@text = text
|
|
143
|
-
@variant = variant
|
|
144
|
-
@pill = pill
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def call
|
|
148
|
-
tag.span(@text, class: ["inline-flex items-center px-2.5 py-0.5 text-xs font-medium",
|
|
149
|
-
@pill ? "rounded-full" : "rounded",
|
|
150
|
-
VARIANTS.fetch(@variant)].join(" "))
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
---
|
|
156
|
-
|
|
157
|
-
## Conditional Rendering
|
|
158
|
-
|
|
159
|
-
```ruby
|
|
160
|
-
class EmptyStateComponent < ViewComponent::Base
|
|
161
|
-
def initialize(collection:, message: "No items found.")
|
|
162
|
-
@collection = collection
|
|
163
|
-
@message = message
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def render?
|
|
167
|
-
@collection.empty?
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
---
|
|
173
|
-
|
|
174
|
-
## Stimulus Integration
|
|
175
|
-
|
|
176
|
-
```ruby
|
|
177
|
-
class ModalComponent < ViewComponent::Base
|
|
178
|
-
renders_one :trigger
|
|
179
|
-
renders_one :body
|
|
180
|
-
|
|
181
|
-
SIZES = { sm: "max-w-sm", md: "max-w-lg", lg: "max-w-2xl" }.freeze
|
|
182
|
-
|
|
183
|
-
def initialize(title:, size: :md)
|
|
184
|
-
@title = title
|
|
185
|
-
@size = size
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
```erb
|
|
191
|
-
<%# app/components/modal_component.html.erb %>
|
|
192
|
-
<div data-controller="modal">
|
|
193
|
-
<div data-action="click->modal#open"><%= trigger %></div>
|
|
194
|
-
<template data-modal-target="dialog">
|
|
195
|
-
<div class="fixed inset-0 z-50" role="dialog" aria-modal="true">
|
|
196
|
-
<div class="fixed inset-0 bg-black/50" data-action="click->modal#close"></div>
|
|
197
|
-
<div class="relative mx-auto mt-20 <%= SIZES.fetch(@size) %> bg-white rounded-lg shadow-xl">
|
|
198
|
-
<div class="flex items-center justify-between px-6 py-4 border-b">
|
|
199
|
-
<h3 class="text-lg font-semibold"><%= @title %></h3>
|
|
200
|
-
<button data-action="modal#close" class="text-gray-400 hover:text-gray-600">×</button>
|
|
201
|
-
</div>
|
|
202
|
-
<div class="p-6"><%= body %></div>
|
|
203
|
-
</div>
|
|
204
|
-
</div>
|
|
205
|
-
</template>
|
|
206
|
-
</div>
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
---
|
|
210
|
-
|
|
211
|
-
## Form Field Component
|
|
212
|
-
|
|
213
|
-
```ruby
|
|
214
|
-
class FormFieldComponent < ViewComponent::Base
|
|
215
|
-
renders_one :hint
|
|
216
|
-
|
|
217
|
-
def initialize(form:, field:, label: nil, required: false, **input_options)
|
|
218
|
-
@form = form
|
|
219
|
-
@field = field
|
|
220
|
-
@label = label
|
|
221
|
-
@required = required
|
|
222
|
-
@input_options = input_options
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def has_errors? = @form.object.errors[@field].any?
|
|
226
|
-
def error_messages = @form.object.errors[@field]
|
|
227
|
-
end
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
```erb
|
|
231
|
-
<div class="mb-4">
|
|
232
|
-
<%= @form.label @field, @label, class: "block text-sm font-medium text-gray-700 mb-1" %>
|
|
233
|
-
<%= @form.text_field @field, class: [
|
|
234
|
-
"block w-full rounded-md border px-3 py-2 text-sm shadow-sm focus:ring-2 focus:ring-blue-500",
|
|
235
|
-
has_errors? ? "border-red-300" : "border-gray-300"
|
|
236
|
-
].join(" "), required: @required, **@input_options %>
|
|
237
|
-
<% if hint? %><p class="mt-1 text-sm text-gray-500"><%= hint %></p><% end %>
|
|
238
|
-
<% error_messages.each do |msg| %>
|
|
239
|
-
<p class="mt-1 text-sm text-red-600"><%= msg %></p>
|
|
240
|
-
<% end %>
|
|
241
|
-
</div>
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
---
|
|
245
|
-
|
|
246
|
-
## Lookbook Previews
|
|
247
|
-
|
|
248
|
-
```ruby
|
|
249
|
-
# app/components/previews/button_component_preview.rb
|
|
250
|
-
class ButtonComponentPreview < Lookbook::Preview
|
|
251
|
-
# @label Default
|
|
252
|
-
def default
|
|
253
|
-
render ButtonComponent.new(text: "Click Me")
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
# @label Variants
|
|
257
|
-
def variants
|
|
258
|
-
render_with_template
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
# @label Disabled
|
|
262
|
-
def disabled
|
|
263
|
-
render ButtonComponent.new(text: "Disabled", disabled: true)
|
|
264
|
-
end
|
|
265
|
-
end
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
```erb
|
|
269
|
-
<%# app/components/previews/button_component_preview/variants.html.erb %>
|
|
270
|
-
<div class="flex gap-4 items-center">
|
|
271
|
-
<%= render ButtonComponent.new(text: "Primary", variant: :primary) %>
|
|
272
|
-
<%= render ButtonComponent.new(text: "Secondary", variant: :secondary) %>
|
|
273
|
-
<%= render ButtonComponent.new(text: "Danger", variant: :danger) %>
|
|
274
|
-
<%= render ButtonComponent.new(text: "Ghost", variant: :ghost) %>
|
|
275
|
-
</div>
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
---
|
|
279
|
-
|
|
280
|
-
## Testing with Minitest (ViewComponent::TestCase)
|
|
281
|
-
|
|
282
|
-
### Button Tests
|
|
283
|
-
|
|
284
|
-
```ruby
|
|
285
|
-
# test/components/button_component_test.rb
|
|
286
|
-
require "test_helper"
|
|
287
|
-
|
|
288
|
-
class ButtonComponentTest < ViewComponent::TestCase
|
|
289
|
-
test "renders with text" do
|
|
290
|
-
render_inline(ButtonComponent.new(text: "Save"))
|
|
291
|
-
assert_selector "button", text: "Save"
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
test "renders with block content" do
|
|
295
|
-
render_inline(ButtonComponent.new) { "Click Me" }
|
|
296
|
-
assert_selector "button", text: "Click Me"
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
test "applies primary variant by default" do
|
|
300
|
-
render_inline(ButtonComponent.new(text: "Save"))
|
|
301
|
-
assert_selector "button.bg-blue-600"
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
test "applies danger variant" do
|
|
305
|
-
render_inline(ButtonComponent.new(text: "Delete", variant: :danger))
|
|
306
|
-
assert_selector "button.bg-red-600"
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
test "renders disabled state" do
|
|
310
|
-
render_inline(ButtonComponent.new(text: "Save", disabled: true))
|
|
311
|
-
assert_selector "button[disabled]"
|
|
312
|
-
assert_selector "button.opacity-50"
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
test "passes html options" do
|
|
316
|
-
render_inline(ButtonComponent.new(text: "Save", id: "save-btn"))
|
|
317
|
-
assert_selector "button#save-btn"
|
|
318
|
-
end
|
|
319
|
-
end
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
### Slot Tests
|
|
323
|
-
|
|
324
|
-
```ruby
|
|
325
|
-
# test/components/card_component_test.rb
|
|
326
|
-
require "test_helper"
|
|
327
|
-
|
|
328
|
-
class CardComponentTest < ViewComponent::TestCase
|
|
329
|
-
test "renders header slot" do
|
|
330
|
-
render_inline(CardComponent.new) do |card|
|
|
331
|
-
card.with_header { "Title" }
|
|
332
|
-
card.with_body { "Content" }
|
|
333
|
-
end
|
|
334
|
-
assert_selector ".border-b", text: "Title"
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
test "renders without header" do
|
|
338
|
-
render_inline(CardComponent.new) do |card|
|
|
339
|
-
card.with_body { "Body only" }
|
|
340
|
-
end
|
|
341
|
-
assert_no_selector ".border-b"
|
|
342
|
-
end
|
|
343
|
-
|
|
344
|
-
test "renders multiple actions" do
|
|
345
|
-
render_inline(CardComponent.new) do |card|
|
|
346
|
-
card.with_body { "Content" }
|
|
347
|
-
card.with_action { "Save" }
|
|
348
|
-
card.with_action { "Cancel" }
|
|
349
|
-
end
|
|
350
|
-
assert_text "Save"
|
|
351
|
-
assert_text "Cancel"
|
|
352
|
-
end
|
|
353
|
-
|
|
354
|
-
test "applies elevated variant" do
|
|
355
|
-
render_inline(CardComponent.new(variant: :elevated)) do |card|
|
|
356
|
-
card.with_body { "Content" }
|
|
357
|
-
end
|
|
358
|
-
assert_selector ".shadow-lg"
|
|
359
|
-
end
|
|
360
|
-
end
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
### Conditional Rendering Test
|
|
364
|
-
|
|
365
|
-
```ruby
|
|
366
|
-
# test/components/empty_state_component_test.rb
|
|
367
|
-
require "test_helper"
|
|
368
|
-
|
|
369
|
-
class EmptyStateComponentTest < ViewComponent::TestCase
|
|
370
|
-
test "renders when collection is empty" do
|
|
371
|
-
render_inline(EmptyStateComponent.new(collection: []))
|
|
372
|
-
assert_text "No items found."
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
test "does not render when collection has items" do
|
|
376
|
-
render_inline(EmptyStateComponent.new(collection: ["item"]))
|
|
377
|
-
assert_no_text "No items found."
|
|
378
|
-
end
|
|
379
|
-
end
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
### Stimulus Integration Test
|
|
383
|
-
|
|
384
|
-
```ruby
|
|
385
|
-
# test/components/modal_component_test.rb
|
|
386
|
-
require "test_helper"
|
|
387
|
-
|
|
388
|
-
class ModalComponentTest < ViewComponent::TestCase
|
|
389
|
-
test "applies stimulus controller" do
|
|
390
|
-
render_inline(ModalComponent.new(title: "Confirm")) do |m|
|
|
391
|
-
m.with_trigger { "Open" }
|
|
392
|
-
m.with_body { "Content" }
|
|
393
|
-
end
|
|
394
|
-
assert_selector '[data-controller="modal"]'
|
|
395
|
-
end
|
|
396
|
-
|
|
397
|
-
test "trigger has open action" do
|
|
398
|
-
render_inline(ModalComponent.new(title: "Confirm")) do |m|
|
|
399
|
-
m.with_trigger { "Open" }
|
|
400
|
-
m.with_body { "Content" }
|
|
401
|
-
end
|
|
402
|
-
assert_selector '[data-action="click->modal#open"]'
|
|
403
|
-
end
|
|
404
|
-
end
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
---
|
|
408
|
-
|
|
409
|
-
## Checklist
|
|
410
|
-
|
|
411
|
-
- [ ] Component has single responsibility
|
|
412
|
-
- [ ] Keyword arguments with sensible defaults
|
|
413
|
-
- [ ] Slots for flexible content areas
|
|
414
|
-
- [ ] `#render?` for conditional rendering
|
|
415
|
-
- [ ] Tailwind classes via private helper methods
|
|
416
|
-
- [ ] Tests cover all variants, slots, edge cases
|
|
417
|
-
- [ ] Lookbook previews for all states
|
|
418
|
-
- [ ] No business logic or data mutations
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# Rails Best Practices Audit
|
|
2
|
-
|
|
3
|
-
Perform a comprehensive Rails best practices audit of the entire codebase. Use the rails-specific skills and agents to identify opportunities to simplify, refactor, and align with Rails conventions ("the Rails Way").
|
|
4
|
-
|
|
5
|
-
## Usage
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
/rails-audit # Full audit of entire codebase
|
|
9
|
-
/rails-audit models only # Scope to specific layer
|
|
10
|
-
/rails-audit --changed-only # Only audit changed files (git diff)
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Instructions
|
|
14
|
-
|
|
15
|
-
Launch a team of agents in parallel to explore the codebase across multiple dimensions. Each agent should use the relevant rails skills (see CLAUDE.md Skill Catalog) and search the web for best practices when skills are insufficient.
|
|
16
|
-
|
|
17
|
-
### Agent 1: Models & Concerns (rails-review agent)
|
|
18
|
-
- Fat-model anti-patterns: models doing too much vs. missing scopes/validations
|
|
19
|
-
- Concern hygiene: single-purpose? overused? should any be model methods?
|
|
20
|
-
- Business logic placement: logic in controllers/services that belongs in models
|
|
21
|
-
- ActiveRecord anti-patterns: raw SQL where scopes suffice, missing `includes`/`preload`
|
|
22
|
-
- Missing validations, unnecessary callbacks, state management patterns
|
|
23
|
-
- Check for state-as-records pattern compliance (booleans vs. state records)
|
|
24
|
-
|
|
25
|
-
### Agent 2: Controllers & Routes (rails-review agent)
|
|
26
|
-
- Everything-is-CRUD compliance: custom actions that should be separate resources
|
|
27
|
-
- Business logic leaking into controllers
|
|
28
|
-
- Strong parameters, before_actions, proper response handling
|
|
29
|
-
- RESTful route compliance (no custom `member`/`collection` verbs)
|
|
30
|
-
- Controller concerns: well-focused or kitchen-sink?
|
|
31
|
-
|
|
32
|
-
### Agent 3: Services, Jobs & Pipeline (rails-review agent)
|
|
33
|
-
- Service objects: single-responsibility, Result pattern compliance
|
|
34
|
-
- Job shallowness: jobs should only deserialize + delegate, no business logic
|
|
35
|
-
- Service objects that should be model methods or concerns (< 3 models = model method)
|
|
36
|
-
- Pipeline stages: consolidation opportunities, error handling
|
|
37
|
-
- Query objects: are complex queries properly extracted?
|
|
38
|
-
|
|
39
|
-
### Agent 4: Views, Frontend & Hotwire (rails-review agent)
|
|
40
|
-
- Turbo Frame/Stream best practices
|
|
41
|
-
- Stimulus controllers: small, focused, one behavior each
|
|
42
|
-
- View logic that should be presenters (SimpleDelegator) or ViewComponents
|
|
43
|
-
- Tailwind CSS patterns: repeated utility groups that should be components
|
|
44
|
-
- Partial organization and reuse
|
|
45
|
-
|
|
46
|
-
### Agent 5: Testing & Quality (rails-review agent)
|
|
47
|
-
- Test DRYness: repeated setup that should be helpers
|
|
48
|
-
- Factory helper consistency (`create_source!`, etc.)
|
|
49
|
-
- Testing behavior vs. implementation
|
|
50
|
-
- Missing coverage patterns (validations, scopes, edge cases)
|
|
51
|
-
- Test isolation and parallel-safety
|
|
52
|
-
|
|
53
|
-
## Output
|
|
54
|
-
|
|
55
|
-
Produce a markdown file at `RAILS_AUDIT.md` in the project root with:
|
|
56
|
-
|
|
57
|
-
```markdown
|
|
58
|
-
# Rails Best Practices Audit — [date]
|
|
59
|
-
|
|
60
|
-
## Executive Summary
|
|
61
|
-
[High-level findings count by severity]
|
|
62
|
-
|
|
63
|
-
## Findings by Category
|
|
64
|
-
|
|
65
|
-
### Category Name
|
|
66
|
-
#### Finding Title
|
|
67
|
-
- **Severity:** high/medium/low
|
|
68
|
-
- **File(s):** `path/to/file.rb:line_number`
|
|
69
|
-
- **Current:** [what it does now]
|
|
70
|
-
- **Recommended:** [what it should do, with code example if helpful]
|
|
71
|
-
- **Rationale:** [why this is better, link to Rails convention]
|
|
72
|
-
- **Effort:** quick (< 30 min) / short (< 2 hrs) / medium (< 1 day) / large (> 1 day)
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Sort findings within each category by severity (high first), then by effort (quick first).
|
|
76
|
-
|
|
77
|
-
$ARGUMENTS
|