source_monitor 0.2.1 → 0.3.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.
Files changed (192) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/agents/rails-concern.md +464 -0
  3. data/.claude/agents/rails-controller.md +424 -0
  4. data/.claude/agents/rails-hotwire.md +446 -0
  5. data/.claude/agents/rails-implement.md +374 -0
  6. data/.claude/agents/rails-job.md +334 -0
  7. data/.claude/agents/rails-lint.md +294 -0
  8. data/.claude/agents/rails-mailer.md +371 -0
  9. data/.claude/agents/rails-migration.md +449 -0
  10. data/.claude/agents/rails-model.md +420 -0
  11. data/.claude/agents/rails-policy.md +443 -0
  12. data/.claude/agents/rails-presenter.md +427 -0
  13. data/.claude/agents/rails-query.md +412 -0
  14. data/.claude/agents/rails-review.md +490 -0
  15. data/.claude/agents/rails-service.md +458 -0
  16. data/.claude/agents/rails-state-records.md +465 -0
  17. data/.claude/agents/rails-tdd.md +314 -0
  18. data/.claude/agents/rails-test.md +441 -0
  19. data/.claude/agents/rails-view-component.md +418 -0
  20. data/.claude/hooks/block-secrets.sh +52 -0
  21. data/.claude/settings.json +85 -0
  22. data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
  23. data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
  24. data/.claude/skills/active-storage-setup/SKILL.md +311 -0
  25. data/.claude/skills/api-versioning/SKILL.md +294 -0
  26. data/.claude/skills/authentication-flow/SKILL.md +335 -0
  27. data/.claude/skills/authentication-flow/reference/current.md +248 -0
  28. data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
  29. data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
  30. data/.claude/skills/authorization-pundit/SKILL.md +462 -0
  31. data/.claude/skills/caching-strategies/SKILL.md +350 -0
  32. data/.claude/skills/database-migrations/SKILL.md +354 -0
  33. data/.claude/skills/form-object-patterns/SKILL.md +399 -0
  34. data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
  35. data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
  36. data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
  37. data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
  38. data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
  39. data/.claude/skills/i18n-patterns/SKILL.md +320 -0
  40. data/.claude/skills/install/SKILL.md +367 -0
  41. data/.claude/skills/performance-optimization/SKILL.md +311 -0
  42. data/.claude/skills/rails-architecture/SKILL.md +259 -0
  43. data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
  44. data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
  45. data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
  46. data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
  47. data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
  48. data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
  49. data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
  50. data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
  51. data/.claude/skills/rails-concern/SKILL.md +399 -0
  52. data/.claude/skills/rails-controller/SKILL.md +336 -0
  53. data/.claude/skills/rails-model-generator/SKILL.md +321 -0
  54. data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
  55. data/.claude/skills/rails-presenter/SKILL.md +274 -0
  56. data/.claude/skills/rails-query-object/SKILL.md +289 -0
  57. data/.claude/skills/rails-service-object/SKILL.md +349 -0
  58. data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
  59. data/.claude/skills/tdd-cycle/SKILL.md +359 -0
  60. data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
  61. data/.rubocop.yml +2 -0
  62. data/.ruby-version +1 -1
  63. data/.vbw-planning/.notification-log.jsonl +192 -0
  64. data/.vbw-planning/.session-log.jsonl +871 -0
  65. data/.vbw-planning/PROJECT.md +51 -0
  66. data/.vbw-planning/REQUIREMENTS.md +50 -0
  67. data/.vbw-planning/SHIPPED.md +28 -0
  68. data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
  69. data/.vbw-planning/codebase/CONCERNS.md +99 -0
  70. data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
  71. data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
  72. data/.vbw-planning/codebase/INDEX.md +86 -0
  73. data/.vbw-planning/codebase/META.md +42 -0
  74. data/.vbw-planning/codebase/PATTERNS.md +262 -0
  75. data/.vbw-planning/codebase/STACK.md +101 -0
  76. data/.vbw-planning/codebase/STRUCTURE.md +324 -0
  77. data/.vbw-planning/codebase/TESTING.md +154 -0
  78. data/.vbw-planning/config.json +12 -0
  79. data/.vbw-planning/discovery.json +24 -0
  80. data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
  81. data/.vbw-planning/milestones/default/STATE.md +83 -0
  82. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
  83. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
  84. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
  85. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
  86. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
  87. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
  88. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
  89. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
  90. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
  91. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
  92. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
  93. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
  94. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
  95. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
  96. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
  97. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
  98. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
  99. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
  100. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
  101. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
  102. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
  103. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
  104. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
  105. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
  106. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
  107. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
  108. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
  109. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
  110. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
  111. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
  112. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
  113. data/CHANGELOG.md +28 -0
  114. data/CLAUDE.md +179 -0
  115. data/Gemfile +8 -0
  116. data/Gemfile.lock +113 -100
  117. data/Rakefile +2 -0
  118. data/app/controllers/source_monitor/application_controller.rb +2 -0
  119. data/app/controllers/source_monitor/health_controller.rb +2 -0
  120. data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
  121. data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
  122. data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
  123. data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
  124. data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
  125. data/app/controllers/source_monitor/items_controller.rb +2 -0
  126. data/app/controllers/source_monitor/sources_controller.rb +0 -14
  127. data/app/helpers/source_monitor/application_helper.rb +4 -112
  128. data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
  129. data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
  130. data/app/jobs/source_monitor/application_job.rb +2 -0
  131. data/app/models/source_monitor/application_record.rb +2 -0
  132. data/app/models/source_monitor/log_entry.rb +0 -2
  133. data/config/coverage_baseline.json +217 -1862
  134. data/config/routes.rb +2 -0
  135. data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
  136. data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
  137. data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
  138. data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
  139. data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
  140. data/lib/source_monitor/assets/bundler.rb +2 -0
  141. data/lib/source_monitor/assets.rb +2 -0
  142. data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
  143. data/lib/source_monitor/configuration/events.rb +60 -0
  144. data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
  145. data/lib/source_monitor/configuration/health_settings.rb +27 -0
  146. data/lib/source_monitor/configuration/http_settings.rb +43 -0
  147. data/lib/source_monitor/configuration/model_definition.rb +108 -0
  148. data/lib/source_monitor/configuration/models.rb +36 -0
  149. data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
  150. data/lib/source_monitor/configuration/retention_settings.rb +45 -0
  151. data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
  152. data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
  153. data/lib/source_monitor/configuration/validation_definition.rb +32 -0
  154. data/lib/source_monitor/configuration.rb +12 -579
  155. data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
  156. data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
  157. data/lib/source_monitor/dashboard/queries.rb +2 -195
  158. data/lib/source_monitor/engine.rb +2 -0
  159. data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
  160. data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
  161. data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
  162. data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
  163. data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
  164. data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
  165. data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
  166. data/lib/source_monitor/items/item_creator.rb +28 -455
  167. data/lib/source_monitor/setup/bundle_installer.rb +2 -0
  168. data/lib/source_monitor/setup/cli.rb +2 -0
  169. data/lib/source_monitor/setup/dependency_checker.rb +2 -0
  170. data/lib/source_monitor/setup/detectors.rb +2 -0
  171. data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
  172. data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
  173. data/lib/source_monitor/setup/install_generator.rb +2 -0
  174. data/lib/source_monitor/setup/migration_installer.rb +2 -0
  175. data/lib/source_monitor/setup/node_installer.rb +2 -0
  176. data/lib/source_monitor/setup/prompter.rb +2 -0
  177. data/lib/source_monitor/setup/requirements.rb +2 -0
  178. data/lib/source_monitor/setup/shell_runner.rb +2 -0
  179. data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
  180. data/lib/source_monitor/setup/verification/printer.rb +2 -0
  181. data/lib/source_monitor/setup/verification/result.rb +2 -0
  182. data/lib/source_monitor/setup/verification/runner.rb +2 -0
  183. data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
  184. data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
  185. data/lib/source_monitor/setup/workflow.rb +2 -0
  186. data/lib/source_monitor/version.rb +3 -1
  187. data/lib/source_monitor.rb +140 -58
  188. data/lib/tasks/source_monitor_assets.rake +2 -0
  189. data/lib/tasks/source_monitor_setup.rake +2 -0
  190. data/lib/tasks/source_monitor_tasks.rake +2 -0
  191. data/source_monitor.gemspec +3 -1
  192. metadata +141 -4
@@ -0,0 +1,418 @@
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">&times;</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
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # PreToolUse hook: Block access to sensitive Rails files.
4
+ # Exit codes: 0 = allow, 2 = block
5
+ #
6
+
7
+ INPUT=$(cat)
8
+
9
+ # Extract file_path from JSON input
10
+ FILE_PATH=$(echo "$INPUT" | ruby -rjson -e 'puts JSON.parse(STDIN.read).dig("tool_input", "file_path").to_s' 2>/dev/null || echo "")
11
+
12
+ if [ -z "$FILE_PATH" ]; then
13
+ exit 0
14
+ fi
15
+
16
+ BASENAME=$(basename "$FILE_PATH")
17
+
18
+ # Block .env files
19
+ case "$BASENAME" in
20
+ .env|.env.*)
21
+ echo "BLOCKED: Environment file access ($BASENAME)" >&2
22
+ echo "Use .env.example for templates. Use Rails credentials for secrets." >&2
23
+ exit 2
24
+ ;;
25
+ esac
26
+
27
+ # Block Rails credentials/keys
28
+ case "$FILE_PATH" in
29
+ *config/master.key|*config/credentials.yml.enc|*config/credentials/*.key)
30
+ echo "BLOCKED: Rails credentials file ($BASENAME)" >&2
31
+ echo "Use: bin/rails credentials:edit" >&2
32
+ exit 2
33
+ ;;
34
+ esac
35
+
36
+ # Block Kamal secrets
37
+ case "$FILE_PATH" in
38
+ *.kamal/secrets)
39
+ echo "BLOCKED: Kamal secrets file" >&2
40
+ exit 2
41
+ ;;
42
+ esac
43
+
44
+ # Block private keys
45
+ case "$BASENAME" in
46
+ *.pem|*.key|*.p12|*.pfx|id_rsa|id_ed25519|id_ecdsa)
47
+ echo "BLOCKED: Private key file ($BASENAME)" >&2
48
+ exit 2
49
+ ;;
50
+ esac
51
+
52
+ exit 0
@@ -0,0 +1,85 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(bin/rails test:*)",
5
+ "Bash(bin/rails test)",
6
+ "Bash(bin/rails generate:*)",
7
+ "Bash(bin/rails g:*)",
8
+ "Bash(bin/rails destroy:*)",
9
+ "Bash(bin/rails d:*)",
10
+ "Bash(bin/rails db:migrate:*)",
11
+ "Bash(bin/rails db:migrate)",
12
+ "Bash(bin/rails db:rollback:*)",
13
+ "Bash(bin/rails db:seed:*)",
14
+ "Bash(bin/rails db:schema:*)",
15
+ "Bash(bin/rails db:test:prepare:*)",
16
+ "Bash(bin/rails routes:*)",
17
+ "Bash(bin/rails console:*)",
18
+ "Bash(bin/rails runner:*)",
19
+ "Bash(bin/rails assets:*)",
20
+ "Bash(bin/rails tmp:*)",
21
+ "Bash(bin/rails log:*)",
22
+ "Bash(bin/rails server:*)",
23
+ "Bash(bin/dev:*)",
24
+ "Bash(bin/setup:*)",
25
+ "Bash(bin/ci:*)",
26
+ "Bash(bin/rubocop:*)",
27
+ "Bash(bin/brakeman:*)",
28
+ "Bash(bin/bundler-audit:*)",
29
+ "Bash(bundle exec:*)",
30
+ "Bash(bundle install:*)",
31
+ "Bash(bundle update:*)",
32
+ "Bash(bundle check:*)",
33
+ "Bash(bundle add:*)",
34
+ "Bash(bundle audit:*)",
35
+ "Bash(git status:*)",
36
+ "Bash(git log:*)",
37
+ "Bash(git diff:*)",
38
+ "Bash(git show:*)",
39
+ "Bash(git branch:*)",
40
+ "Bash(git stash:*)",
41
+ "Bash(ls:*)",
42
+ "Bash(tree:*)",
43
+ "Bash(wc:*)",
44
+ "Bash(which:*)",
45
+ "Bash(pwd:*)",
46
+ "Bash(yarn:*)",
47
+ "Bash(npm:*)",
48
+ "Bash(npx:*)",
49
+ "Bash(ruby:*)",
50
+ "Bash(bin/importmap:*)"
51
+ ],
52
+ "deny": [
53
+ "Bash(git push --force:*)",
54
+ "Bash(git push -f:*)",
55
+ "Bash(git reset --hard:*)",
56
+ "Bash(sudo:*)",
57
+ "Bash(rm -rf /:*)",
58
+ "Bash(rm -rf ~:*)",
59
+ "Bash(rm -rf ..:*)",
60
+ "Bash(chmod 777:*)",
61
+ "Read(.env)",
62
+ "Read(.env.*)",
63
+ "Read(config/master.key)",
64
+ "Read(config/credentials.yml.enc)",
65
+ "Read(.kamal/secrets)"
66
+ ]
67
+ },
68
+ "hooks": {
69
+ "PreToolUse": [
70
+ {
71
+ "matcher": "Read|Edit|Write",
72
+ "hooks": [
73
+ {
74
+ "type": "command",
75
+ "command": "bash .claude/hooks/block-secrets.sh"
76
+ }
77
+ ]
78
+ }
79
+ ]
80
+ },
81
+ "enabledPlugins": {
82
+ "frontend-design@claude-plugins-official": true,
83
+ "vbw@vbw-marketplace": true
84
+ }
85
+ }