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.
- checksums.yaml +4 -4
- data/.claude/agents/rails-concern.md +464 -0
- data/.claude/agents/rails-controller.md +424 -0
- data/.claude/agents/rails-hotwire.md +446 -0
- data/.claude/agents/rails-implement.md +374 -0
- data/.claude/agents/rails-job.md +334 -0
- data/.claude/agents/rails-lint.md +294 -0
- data/.claude/agents/rails-mailer.md +371 -0
- data/.claude/agents/rails-migration.md +449 -0
- data/.claude/agents/rails-model.md +420 -0
- data/.claude/agents/rails-policy.md +443 -0
- data/.claude/agents/rails-presenter.md +427 -0
- data/.claude/agents/rails-query.md +412 -0
- data/.claude/agents/rails-review.md +490 -0
- data/.claude/agents/rails-service.md +458 -0
- data/.claude/agents/rails-state-records.md +465 -0
- data/.claude/agents/rails-tdd.md +314 -0
- data/.claude/agents/rails-test.md +441 -0
- data/.claude/agents/rails-view-component.md +418 -0
- data/.claude/hooks/block-secrets.sh +52 -0
- data/.claude/settings.json +85 -0
- data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
- data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
- data/.claude/skills/active-storage-setup/SKILL.md +311 -0
- data/.claude/skills/api-versioning/SKILL.md +294 -0
- data/.claude/skills/authentication-flow/SKILL.md +335 -0
- data/.claude/skills/authentication-flow/reference/current.md +248 -0
- data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
- data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
- data/.claude/skills/authorization-pundit/SKILL.md +462 -0
- data/.claude/skills/caching-strategies/SKILL.md +350 -0
- data/.claude/skills/database-migrations/SKILL.md +354 -0
- data/.claude/skills/form-object-patterns/SKILL.md +399 -0
- data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
- data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
- data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
- data/.claude/skills/i18n-patterns/SKILL.md +320 -0
- data/.claude/skills/install/SKILL.md +367 -0
- data/.claude/skills/performance-optimization/SKILL.md +311 -0
- data/.claude/skills/rails-architecture/SKILL.md +259 -0
- data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
- data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
- data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
- data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
- data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
- data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
- data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
- data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
- data/.claude/skills/rails-concern/SKILL.md +399 -0
- data/.claude/skills/rails-controller/SKILL.md +336 -0
- data/.claude/skills/rails-model-generator/SKILL.md +321 -0
- data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
- data/.claude/skills/rails-presenter/SKILL.md +274 -0
- data/.claude/skills/rails-query-object/SKILL.md +289 -0
- data/.claude/skills/rails-service-object/SKILL.md +349 -0
- data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
- data/.claude/skills/tdd-cycle/SKILL.md +359 -0
- data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -1
- data/.vbw-planning/.notification-log.jsonl +192 -0
- data/.vbw-planning/.session-log.jsonl +871 -0
- data/.vbw-planning/PROJECT.md +51 -0
- data/.vbw-planning/REQUIREMENTS.md +50 -0
- data/.vbw-planning/SHIPPED.md +28 -0
- data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
- data/.vbw-planning/codebase/CONCERNS.md +99 -0
- data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
- data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
- data/.vbw-planning/codebase/INDEX.md +86 -0
- data/.vbw-planning/codebase/META.md +42 -0
- data/.vbw-planning/codebase/PATTERNS.md +262 -0
- data/.vbw-planning/codebase/STACK.md +101 -0
- data/.vbw-planning/codebase/STRUCTURE.md +324 -0
- data/.vbw-planning/codebase/TESTING.md +154 -0
- data/.vbw-planning/config.json +12 -0
- data/.vbw-planning/discovery.json +24 -0
- data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
- data/.vbw-planning/milestones/default/STATE.md +83 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +179 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +113 -100
- data/Rakefile +2 -0
- data/app/controllers/source_monitor/application_controller.rb +2 -0
- data/app/controllers/source_monitor/health_controller.rb +2 -0
- data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
- data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
- data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
- data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
- data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
- data/app/controllers/source_monitor/items_controller.rb +2 -0
- data/app/controllers/source_monitor/sources_controller.rb +0 -14
- data/app/helpers/source_monitor/application_helper.rb +4 -112
- data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
- data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
- data/app/jobs/source_monitor/application_job.rb +2 -0
- data/app/models/source_monitor/application_record.rb +2 -0
- data/app/models/source_monitor/log_entry.rb +0 -2
- data/config/coverage_baseline.json +217 -1862
- data/config/routes.rb +2 -0
- data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
- data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
- data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
- data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
- data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
- data/lib/source_monitor/assets/bundler.rb +2 -0
- data/lib/source_monitor/assets.rb +2 -0
- data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
- data/lib/source_monitor/configuration/events.rb +60 -0
- data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
- data/lib/source_monitor/configuration/health_settings.rb +27 -0
- data/lib/source_monitor/configuration/http_settings.rb +43 -0
- data/lib/source_monitor/configuration/model_definition.rb +108 -0
- data/lib/source_monitor/configuration/models.rb +36 -0
- data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
- data/lib/source_monitor/configuration/retention_settings.rb +45 -0
- data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
- data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
- data/lib/source_monitor/configuration/validation_definition.rb +32 -0
- data/lib/source_monitor/configuration.rb +12 -579
- data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
- data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
- data/lib/source_monitor/dashboard/queries.rb +2 -195
- data/lib/source_monitor/engine.rb +2 -0
- data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
- data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
- data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
- data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
- data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
- data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
- data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
- data/lib/source_monitor/items/item_creator.rb +28 -455
- data/lib/source_monitor/setup/bundle_installer.rb +2 -0
- data/lib/source_monitor/setup/cli.rb +2 -0
- data/lib/source_monitor/setup/dependency_checker.rb +2 -0
- data/lib/source_monitor/setup/detectors.rb +2 -0
- data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
- data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
- data/lib/source_monitor/setup/install_generator.rb +2 -0
- data/lib/source_monitor/setup/migration_installer.rb +2 -0
- data/lib/source_monitor/setup/node_installer.rb +2 -0
- data/lib/source_monitor/setup/prompter.rb +2 -0
- data/lib/source_monitor/setup/requirements.rb +2 -0
- data/lib/source_monitor/setup/shell_runner.rb +2 -0
- data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/printer.rb +2 -0
- data/lib/source_monitor/setup/verification/result.rb +2 -0
- data/lib/source_monitor/setup/verification/runner.rb +2 -0
- data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
- data/lib/source_monitor/setup/workflow.rb +2 -0
- data/lib/source_monitor/version.rb +3 -1
- data/lib/source_monitor.rb +140 -58
- data/lib/tasks/source_monitor_assets.rake +2 -0
- data/lib/tasks/source_monitor_setup.rake +2 -0
- data/lib/tasks/source_monitor_tasks.rake +2 -0
- data/source_monitor.gemspec +3 -1
- 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">×</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
|
+
}
|