rails-hyperdrive-layered-rails 0.1.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 (62) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +30 -0
  3. data/README.md +89 -0
  4. data/UPSTREAM +4 -0
  5. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/SKILL.md.erb +315 -0
  6. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/authorization-to-policy.md +79 -0
  7. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/callbacks-to-service.md +72 -0
  8. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/complex-input-to-form-object.md +119 -0
  9. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/current-from-model.md +63 -0
  10. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/god-object-decomposition.md +81 -0
  11. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/implicit-to-explicit-state-machine.md +77 -0
  12. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/query-to-query-object.md +63 -0
  13. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/view-logic-to-presenter.md +60 -0
  14. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/callbacks.md +114 -0
  15. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/concerns.md +92 -0
  16. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/helpers.md +66 -0
  17. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/jobs.md +71 -0
  18. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/layer-violations.md +176 -0
  19. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/service-objects.md +131 -0
  20. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/testing.md +37 -0
  21. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/core/architecture-layers.md +204 -0
  22. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/core/extraction-signals.md +314 -0
  23. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/core/specification-test.md +229 -0
  24. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/action-policy.md +437 -0
  25. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/active-agent.md +275 -0
  26. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/active-delivery.md +221 -0
  27. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/active-job-performs.md +171 -0
  28. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/alba.md +257 -0
  29. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/anyway-config.md +212 -0
  30. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/rubanok.md +246 -0
  31. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/view-component.md +223 -0
  32. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/workflow.md +305 -0
  33. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/collaborator-objects.md +209 -0
  34. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/concerns.md +381 -0
  35. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/filter-objects.md +222 -0
  36. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/form-objects.md +267 -0
  37. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/policy-objects.md +298 -0
  38. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/presenters.md +257 -0
  39. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/query-objects.md +208 -0
  40. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/repositories.md +382 -0
  41. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/serializers.md +256 -0
  42. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/service-objects.md +170 -0
  43. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/state-machines.md +368 -0
  44. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/value-objects.md +267 -0
  45. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/ai-integration.md +372 -0
  46. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/authorization.md +398 -0
  47. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/callbacks.md +336 -0
  48. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/configuration.md +383 -0
  49. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/current-attributes.md +307 -0
  50. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/instrumentation.md +386 -0
  51. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/notifications.md +373 -0
  52. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/view-components.md +454 -0
  53. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/analyze-callbacks.md +255 -0
  54. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/analyze-gods.md +279 -0
  55. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/analyze-services.md +1444 -0
  56. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/analyze.md +376 -0
  57. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/plan.md +249 -0
  58. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/review.md +473 -0
  59. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/spec-test.md +282 -0
  60. data/lib/rails-hyperdrive-layered-rails/version.rb +3 -0
  61. data/lib/rails-hyperdrive-layered-rails.rb +7 -0
  62. metadata +106 -0
@@ -0,0 +1,212 @@
1
+ # Anyway Config
2
+
3
+ Configuration management with typed attributes and multiple sources.
4
+
5
+ **GitHub**: https://github.com/palkan/anyway_config
6
+ **Layer**: Infrastructure
7
+
8
+ ## Contents
9
+
10
+ - Installation
11
+ - Basic Usage
12
+ - Base Config Class
13
+ - Nested Configuration
14
+ - Type Coercion
15
+ - Validation
16
+ - Environment Sources (Priority)
17
+ - Local Overrides
18
+ - Testing
19
+ - Rails Credentials
20
+
21
+ ## Installation
22
+
23
+ ```ruby
24
+ # Gemfile
25
+ gem "anyway_config"
26
+
27
+ # Generate config
28
+ rails generate anyway:config stripe
29
+ ```
30
+
31
+ ## Basic Usage
32
+
33
+ ### Define Config
34
+
35
+ ```ruby
36
+ # app/configs/stripe_config.rb
37
+ class StripeConfig < Anyway::Config
38
+ attr_config :api_key,
39
+ :webhook_secret,
40
+ :publishable_key,
41
+ currency: "usd"
42
+
43
+ required :api_key, :webhook_secret
44
+ end
45
+ ```
46
+
47
+ ### YAML Source
48
+
49
+ ```yaml
50
+ # config/stripe.yml
51
+ default:
52
+ currency: usd
53
+
54
+ development:
55
+ api_key: sk_test_xxx
56
+ webhook_secret: whsec_xxx
57
+ publishable_key: pk_test_xxx
58
+
59
+ production:
60
+ api_key: <%= ENV["STRIPE_API_KEY"] %>
61
+ webhook_secret: <%= ENV["STRIPE_WEBHOOK_SECRET"] %>
62
+ publishable_key: <%= ENV["STRIPE_PUBLISHABLE_KEY"] %>
63
+ ```
64
+
65
+ ### Access Config
66
+
67
+ ```ruby
68
+ StripeConfig.new.api_key
69
+ # Or with singleton pattern
70
+ StripeConfig.api_key
71
+ ```
72
+
73
+ ## Base Config Class
74
+
75
+ ```ruby
76
+ # app/configs/application_config.rb
77
+ class ApplicationConfig < Anyway::Config
78
+ class << self
79
+ def instance
80
+ @instance ||= new
81
+ end
82
+
83
+ delegate_missing_to :instance
84
+ end
85
+ end
86
+
87
+ class StripeConfig < ApplicationConfig
88
+ # ...
89
+ end
90
+
91
+ # Usage
92
+ StripeConfig.api_key
93
+ ```
94
+
95
+ ## Nested Configuration
96
+
97
+ ```ruby
98
+ class EmailConfig < ApplicationConfig
99
+ attr_config :from_address,
100
+ :reply_to,
101
+ smtp: {
102
+ host: "localhost",
103
+ port: 25,
104
+ username: nil,
105
+ password: nil
106
+ }
107
+ end
108
+
109
+ # Access
110
+ EmailConfig.smtp.host
111
+ EmailConfig.smtp.port
112
+ ```
113
+
114
+ ## Type Coercion
115
+
116
+ ```ruby
117
+ class AppConfig < ApplicationConfig
118
+ attr_config :port,
119
+ :debug,
120
+ :allowed_hosts
121
+
122
+ coerce_types port: :integer,
123
+ debug: :boolean,
124
+ allowed_hosts: { type: :string, array: true }
125
+ end
126
+ ```
127
+
128
+ ## Validation
129
+
130
+ ```ruby
131
+ class DatabaseConfig < ApplicationConfig
132
+ attr_config :host, :port, :pool_size
133
+
134
+ required :host
135
+
136
+ on_load do
137
+ raise_validation_error("port must be positive") if port && port <= 0
138
+ raise_validation_error("pool_size must be positive") if pool_size <= 0
139
+ end
140
+ end
141
+ ```
142
+
143
+ ## Environment Sources (Priority)
144
+
145
+ 1. **Environment variables** (highest)
146
+ - `STRIPE_API_KEY`, `STRIPE_CURRENCY`
147
+
148
+ 2. **Local files** (gitignored)
149
+ - `config/stripe.local.yml`
150
+
151
+ 3. **Environment YAML**
152
+ - `config/stripe.yml` (under environment key)
153
+
154
+ 4. **Default values** (lowest)
155
+ - `attr_config currency: "usd"`
156
+
157
+ ```ruby
158
+ # Override via ENV
159
+ STRIPE_CURRENCY=eur rails console
160
+ StripeConfig.currency #=> "eur"
161
+ ```
162
+
163
+ ## Local Overrides
164
+
165
+ ```yaml
166
+ # config/stripe.local.yml (gitignored)
167
+ api_key: sk_test_local_override
168
+ ```
169
+
170
+ ## Testing
171
+
172
+ ```ruby
173
+ RSpec.describe PaymentService do
174
+ let(:config) do
175
+ StripeConfig.new(
176
+ api_key: "test_key",
177
+ webhook_secret: "test_secret",
178
+ currency: "eur"
179
+ )
180
+ end
181
+
182
+ it "uses configured currency" do
183
+ service = described_class.new(config: config)
184
+ # ...
185
+ end
186
+ end
187
+
188
+ # Or with environment override
189
+ RSpec.describe "with config" do
190
+ around do |example|
191
+ with_env("STRIPE_CURRENCY" => "gbp") { example.run }
192
+ end
193
+
194
+ it "uses environment value" do
195
+ expect(StripeConfig.new.currency).to eq("gbp")
196
+ end
197
+ end
198
+ ```
199
+
200
+ ## Rails Credentials
201
+
202
+ ```ruby
203
+ class SecretsConfig < ApplicationConfig
204
+ attr_config :secret_key_base,
205
+ :api_credentials
206
+
207
+ # Load from Rails credentials
208
+ def values
209
+ Rails.application.credentials.to_h
210
+ end
211
+ end
212
+ ```
@@ -0,0 +1,246 @@
1
+ # Rubanok
2
+
3
+ Transform controller params into database queries.
4
+
5
+ **GitHub**: https://github.com/palkan/rubanok
6
+ **Layer**: Presentation/Application boundary
7
+
8
+ ## Contents
9
+
10
+ - Installation
11
+ - Basic Usage
12
+ - Param Mapping with `map`
13
+ - Matching with `match`
14
+ - Failure Strategy for Invalid Params
15
+ - Nested Params
16
+ - Sorting
17
+ - Pagination
18
+ - With Form Object
19
+ - Testing
20
+
21
+ ## Installation
22
+
23
+ ```ruby
24
+ # Gemfile
25
+ gem "rubanok"
26
+ ```
27
+
28
+ ## Basic Usage
29
+
30
+ ### Define Processor
31
+
32
+ ```ruby
33
+ class PostsProcessor < Rubanok::Processor
34
+ # Simple equality match
35
+ map :status
36
+
37
+ # With transformation
38
+ map :author_id do |author_id:|
39
+ raw.where(author_id: author_id)
40
+ end
41
+
42
+ # Search
43
+ map :q do |q:|
44
+ raw.where("title ILIKE ?", "%#{q}%")
45
+ end
46
+
47
+ # Sorting
48
+ map :sort do |sort:|
49
+ field, dir = sort.split("_")
50
+ raw.order(field => dir)
51
+ end
52
+ end
53
+ ```
54
+
55
+ ### Controller Integration
56
+
57
+ ```ruby
58
+ class PostsController < ApplicationController
59
+ def index
60
+ @posts = rubanok_process(Post.all, params)
61
+ # Or with explicit processor
62
+ @posts = rubanok_process(Post.all, params, with: PostsProcessor)
63
+ end
64
+ end
65
+ ```
66
+
67
+ ## Param Mapping with `map`
68
+
69
+ ### Simple Match
70
+
71
+ ```ruby
72
+ class PostsProcessor < Rubanok::Processor
73
+ # Maps params[:status] to where(status: value)
74
+ map :status
75
+
76
+ # With alias
77
+ map :state, to: :status
78
+ end
79
+ ```
80
+
81
+ ### Custom Mapping
82
+
83
+ ```ruby
84
+ class PostsProcessor < Rubanok::Processor
85
+ map :published do |published:|
86
+ if published == "true"
87
+ raw.where.not(published_at: nil)
88
+ else
89
+ raw.where(published_at: nil)
90
+ end
91
+ end
92
+ end
93
+ ```
94
+
95
+ ### Multiple Params
96
+
97
+ ```ruby
98
+ class PostsProcessor < Rubanok::Processor
99
+ map :min_date, :max_date do |min_date: nil, max_date: nil|
100
+ scope = raw
101
+ scope = scope.where("created_at >= ?", min_date) if min_date
102
+ scope = scope.where("created_at <= ?", max_date) if max_date
103
+ scope
104
+ end
105
+ end
106
+ ```
107
+
108
+ ## Matching with `match`
109
+
110
+ ```ruby
111
+ class PostsProcessor < Rubanok::Processor
112
+ # Match specific values
113
+ match :order, ->(dir) { dir.in?(%w[asc desc]) } do |order:|
114
+ raw.order(created_at: order)
115
+ end
116
+
117
+ # Match with type checking
118
+ match :page, ->(v) { v.to_i.positive? } do |page:|
119
+ raw.page(page.to_i)
120
+ end
121
+ end
122
+ ```
123
+
124
+ ## Failure Strategy for Invalid Params
125
+
126
+ ```ruby
127
+ class PostsProcessor < Rubanok::Processor
128
+ # Raise on invalid params
129
+ fail_when :status, ->(v) { !Post.statuses.key?(v) }
130
+
131
+ # Or custom handling
132
+ match :status, ->(v) { Post.statuses.key?(v) } do |status:|
133
+ raw.where(status: status)
134
+ end
135
+ end
136
+ ```
137
+
138
+ ## Nested Params
139
+
140
+ ```ruby
141
+ class PostsProcessor < Rubanok::Processor
142
+ nested :filter do
143
+ map :status
144
+ map :author_id
145
+ end
146
+ end
147
+
148
+ # Processes params[:filter][:status], params[:filter][:author_id]
149
+ ```
150
+
151
+ ## Sorting
152
+
153
+ ```ruby
154
+ class PostsProcessor < Rubanok::Processor
155
+ SORTABLE_FIELDS = %w[title created_at views_count].freeze
156
+
157
+ map :sort_by, :sort_order do |sort_by: "created_at", sort_order: "desc"|
158
+ field = SORTABLE_FIELDS.include?(sort_by) ? sort_by : "created_at"
159
+ dir = sort_order == "asc" ? :asc : :desc
160
+ raw.order(field => dir)
161
+ end
162
+ end
163
+ ```
164
+
165
+ ## Pagination
166
+
167
+ ```ruby
168
+ class PostsProcessor < Rubanok::Processor
169
+ DEFAULT_PER_PAGE = 20
170
+ MAX_PER_PAGE = 100
171
+
172
+ map :page, :per_page do |page: 1, per_page: DEFAULT_PER_PAGE|
173
+ page = [page.to_i, 1].max
174
+ per_page = [[per_page.to_i, 1].max, MAX_PER_PAGE].min
175
+
176
+ raw.offset((page - 1) * per_page).limit(per_page)
177
+ end
178
+ end
179
+ ```
180
+
181
+ ## With Form Object
182
+
183
+ ```ruby
184
+ class PostsFilterForm
185
+ include ActiveModel::Model
186
+ include ActiveModel::Attributes
187
+
188
+ attribute :status, :string
189
+ attribute :author_id, :integer
190
+ attribute :q, :string
191
+ attribute :sort_by, :string, default: "created_at"
192
+ attribute :sort_order, :string, default: "desc"
193
+
194
+ validates :status, inclusion: { in: Post.statuses.keys }, allow_blank: true
195
+ end
196
+
197
+ class PostsController < ApplicationController
198
+ def index
199
+ @filter = PostsFilterForm.new(filter_params)
200
+
201
+ if @filter.valid?
202
+ @posts = rubanok_process(Post.all, @filter.attributes)
203
+ else
204
+ @posts = Post.none
205
+ end
206
+ end
207
+
208
+ private
209
+
210
+ def filter_params
211
+ params.fetch(:filter, {}).permit(:status, :author_id, :q, :sort_by, :sort_order)
212
+ end
213
+ end
214
+ ```
215
+
216
+ ## Testing
217
+
218
+ ```ruby
219
+ RSpec.describe PostsProcessor do
220
+ let(:scope) { Post.all }
221
+
222
+ describe "status filter" do
223
+ it "filters by status" do
224
+ result = described_class.call(scope, status: "published")
225
+
226
+ expect(result.to_sql).to include("status")
227
+ end
228
+ end
229
+
230
+ describe "search" do
231
+ it "searches by title" do
232
+ result = described_class.call(scope, q: "ruby")
233
+
234
+ expect(result.to_sql).to include("ILIKE")
235
+ end
236
+ end
237
+
238
+ describe "sorting" do
239
+ it "sorts by field" do
240
+ result = described_class.call(scope, sort: "title_asc")
241
+
242
+ expect(result.to_sql).to include("ORDER BY")
243
+ end
244
+ end
245
+ end
246
+ ```
@@ -0,0 +1,223 @@
1
+ # ViewComponent
2
+
3
+ Framework for building reusable, testable view components.
4
+
5
+ **GitHub**: https://github.com/viewcomponent/view_component
6
+ **Layer**: Presentation
7
+
8
+ ## Contents
9
+
10
+ - Installation
11
+ - Basic Usage
12
+ - Slots
13
+ - Collections
14
+ - Inline Components
15
+ - Stimulus Integration
16
+ - Previews
17
+ - Testing
18
+
19
+ ## Installation
20
+
21
+ ```ruby
22
+ # Gemfile
23
+ gem "view_component"
24
+
25
+ # Generate component
26
+ rails generate component UserAvatar user size
27
+ ```
28
+
29
+ ## Basic Usage
30
+
31
+ ### Define Component
32
+
33
+ ```ruby
34
+ # app/components/user_avatar_component.rb
35
+ class UserAvatarComponent < ViewComponent::Base
36
+ def initialize(user:, size: :medium)
37
+ @user = user
38
+ @size = size
39
+ end
40
+
41
+ private
42
+
43
+ def size_class
44
+ case @size
45
+ when :small then "w-8 h-8"
46
+ when :medium then "w-12 h-12"
47
+ when :large then "w-16 h-16"
48
+ end
49
+ end
50
+ end
51
+ ```
52
+
53
+ ```erb
54
+ <%# app/components/user_avatar_component.html.erb %>
55
+ <div class="avatar <%= size_class %>">
56
+ <% if @user.avatar.attached? %>
57
+ <%= image_tag @user.avatar, class: "rounded-full" %>
58
+ <% else %>
59
+ <span class="avatar-initials"><%= @user.initials %></span>
60
+ <% end %>
61
+ </div>
62
+ ```
63
+
64
+ ### Render Component
65
+
66
+ ```erb
67
+ <%= render(UserAvatarComponent.new(user: @user, size: :large)) %>
68
+ ```
69
+
70
+ ## Slots
71
+
72
+ ### Single Slot
73
+
74
+ ```ruby
75
+ class CardComponent < ViewComponent::Base
76
+ renders_one :header
77
+ renders_one :footer
78
+ end
79
+ ```
80
+
81
+ ```erb
82
+ <%# Template %>
83
+ <div class="card">
84
+ <% if header? %>
85
+ <header><%= header %></header>
86
+ <% end %>
87
+
88
+ <div class="card-body"><%= content %></div>
89
+
90
+ <% if footer? %>
91
+ <footer><%= footer %></footer>
92
+ <% end %>
93
+ </div>
94
+ ```
95
+
96
+ ```erb
97
+ <%# Usage %>
98
+ <%= render(CardComponent.new) do |card| %>
99
+ <% card.with_header { "Title" } %>
100
+ <p>Body content</p>
101
+ <% card.with_footer { "Footer text" } %>
102
+ <% end %>
103
+ ```
104
+
105
+ ### Multiple Slots
106
+
107
+ ```ruby
108
+ class ListComponent < ViewComponent::Base
109
+ renders_many :items, ItemComponent
110
+
111
+ class ItemComponent < ViewComponent::Base
112
+ def initialize(title:)
113
+ @title = title
114
+ end
115
+ end
116
+ end
117
+ ```
118
+
119
+ ## Collections
120
+
121
+ ```ruby
122
+ class PostComponent < ViewComponent::Base
123
+ with_collection_parameter :post
124
+
125
+ def initialize(post:)
126
+ @post = post
127
+ end
128
+ end
129
+
130
+ # Render collection
131
+ <%= render(PostComponent.with_collection(@posts)) %>
132
+ ```
133
+
134
+ ## Inline Components
135
+
136
+ ```ruby
137
+ class StatusBadgeComponent < ViewComponent::Base
138
+ def initialize(status:)
139
+ @status = status
140
+ end
141
+
142
+ def call
143
+ content_tag :span, @status.titleize, class: "badge #{badge_class}"
144
+ end
145
+
146
+ private
147
+
148
+ def badge_class
149
+ case @status.to_sym
150
+ when :active then "badge-success"
151
+ when :pending then "badge-warning"
152
+ else "badge-secondary"
153
+ end
154
+ end
155
+ end
156
+ ```
157
+
158
+ ## Stimulus Integration
159
+
160
+ ```ruby
161
+ class DropdownComponent < ViewComponent::Base
162
+ def initialize(label:)
163
+ @label = label
164
+ end
165
+ end
166
+ ```
167
+
168
+ ```erb
169
+ <div data-controller="dropdown">
170
+ <button data-action="dropdown#toggle"><%= @label %></button>
171
+ <div data-dropdown-target="menu" class="hidden">
172
+ <%= content %>
173
+ </div>
174
+ </div>
175
+ ```
176
+
177
+ ## Previews
178
+
179
+ ```ruby
180
+ # test/components/previews/user_avatar_component_preview.rb
181
+ class UserAvatarComponentPreview < ViewComponent::Preview
182
+ def small
183
+ render(UserAvatarComponent.new(user: User.first, size: :small))
184
+ end
185
+
186
+ def medium
187
+ render(UserAvatarComponent.new(user: User.first, size: :medium))
188
+ end
189
+
190
+ def large
191
+ render(UserAvatarComponent.new(user: User.first, size: :large))
192
+ end
193
+ end
194
+ ```
195
+
196
+ Access at: `/rails/view_components/user_avatar_component/small`
197
+
198
+ ## Testing
199
+
200
+ ```ruby
201
+ RSpec.describe UserAvatarComponent, type: :component do
202
+ it "renders avatar for user with image" do
203
+ user = create(:user, :with_avatar)
204
+ render_inline(described_class.new(user: user))
205
+
206
+ expect(page).to have_css("img.rounded-full")
207
+ end
208
+
209
+ it "renders initials without image" do
210
+ user = build(:user, name: "John Doe")
211
+ render_inline(described_class.new(user: user))
212
+
213
+ expect(page).to have_css(".avatar-initials", text: "JD")
214
+ end
215
+
216
+ it "applies size class" do
217
+ user = build(:user)
218
+ render_inline(described_class.new(user: user, size: :large))
219
+
220
+ expect(page).to have_css(".w-16.h-16")
221
+ end
222
+ end
223
+ ```