style_capsule 1.3.0 → 2.0.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.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "digest/sha1"
4
+ require_relative "component_class_methods"
4
5
  # ActiveSupport string extensions are conditionally required in lib/style_capsule.rb
5
6
 
6
7
  module StyleCapsule
@@ -95,252 +96,7 @@ module StyleCapsule
95
96
  end
96
97
 
97
98
  module ClassMethods
98
- # Class-level cache for scoped CSS per component class
99
- def css_cache
100
- @css_cache ||= {}
101
- end
102
-
103
- # Clear the CSS cache for this component class
104
- #
105
- # Useful for testing or when you want to force CSS reprocessing.
106
- # In development, this is automatically called when classes are reloaded.
107
- #
108
- # @example
109
- # MyComponent.clear_css_cache
110
- def clear_css_cache
111
- @css_cache = {}
112
- end
113
-
114
- # Set or get a custom capsule ID for this component class (useful for testing)
115
- #
116
- # @param capsule_id [String, nil] The custom capsule ID to use (nil to get current value)
117
- # @return [String, nil] The current capsule ID if no argument provided
118
- # @example Setting a custom capsule ID
119
- # class MyComponent < ApplicationComponent
120
- # include StyleCapsule::Component
121
- # capsule_id "test-capsule-123"
122
- # end
123
- # @example Getting the current capsule ID
124
- # MyComponent.capsule_id # => "test-capsule-123" or nil
125
- def capsule_id(capsule_id = nil)
126
- if capsule_id.nil?
127
- @custom_capsule_id if defined?(@custom_capsule_id)
128
- else
129
- @custom_capsule_id = capsule_id.to_s
130
- end
131
- end
132
-
133
- # Configure stylesheet registry for head rendering
134
- #
135
- # Enables head rendering and configures namespace and cache strategy in a single call.
136
- # All parameters are optional - calling without arguments enables head rendering with defaults.
137
- #
138
- # @param namespace [Symbol, String, nil] Namespace identifier (nil/blank uses default)
139
- # @param cache_strategy [Symbol, String, Proc, nil] Cache strategy: :none (default), :time, :proc, :file
140
- # - Symbol or String: :none, :time, :proc, :file (or "none", "time", "proc", "file")
141
- # - Proc: Custom cache proc (automatically uses :proc strategy)
142
- # Proc receives: (css_content, capsule_id, namespace) and should return [cache_key, should_cache, expires_at]
143
- # @param cache_ttl [Integer, ActiveSupport::Duration, nil] Time-to-live in seconds (for :time strategy). Supports ActiveSupport::Duration (e.g., 1.hour, 30.minutes)
144
- # @param cache_proc [Proc, nil] Custom cache proc (for :proc strategy, ignored if cache_strategy is a Proc)
145
- # Proc receives: (css_content, capsule_id, namespace) and should return [cache_key, should_cache, expires_at]
146
- # @return [void]
147
- # @example Basic usage (enables head rendering with defaults)
148
- # class MyComponent < ApplicationComponent
149
- # include StyleCapsule::Component
150
- # stylesheet_registry
151
- # end
152
- # @example With namespace
153
- # class AdminComponent < ApplicationComponent
154
- # include StyleCapsule::Component
155
- # stylesheet_registry namespace: :admin
156
- # end
157
- # @example With time-based caching
158
- # class MyComponent < ApplicationComponent
159
- # include StyleCapsule::Component
160
- # stylesheet_registry cache_strategy: :time, cache_ttl: 1.hour
161
- # end
162
- # @example With custom proc caching
163
- # class MyComponent < ApplicationComponent
164
- # include StyleCapsule::Component
165
- # stylesheet_registry cache_strategy: :proc, cache_proc: ->(css, capsule_id, ns) {
166
- # cache_key = "css_#{capsule_id}_#{ns}"
167
- # should_cache = css.length > 100
168
- # expires_at = Time.now + 1800
169
- # [cache_key, should_cache, expires_at]
170
- # }
171
- # end
172
- # @example File-based caching (requires class method component_styles)
173
- # class MyComponent < ApplicationComponent
174
- # include StyleCapsule::Component
175
- # stylesheet_registry cache_strategy: :file
176
- #
177
- # def self.component_styles # Must be class method for file caching
178
- # <<~CSS
179
- # .section { color: red; }
180
- # CSS
181
- # end
182
- # end
183
- # @example All options combined
184
- # class MyComponent < ApplicationComponent
185
- # include StyleCapsule::Component
186
- # stylesheet_registry namespace: :admin, cache_strategy: :time, cache_ttl: 1.hour
187
- # end
188
- def stylesheet_registry(namespace: nil, cache_strategy: :none, cache_ttl: nil, cache_proc: nil)
189
- @head_rendering = true
190
- @stylesheet_namespace = namespace unless namespace.nil?
191
-
192
- # Normalize cache_strategy: convert strings to symbols, handle Proc
193
- normalized_strategy, normalized_proc = normalize_cache_strategy(cache_strategy, cache_proc)
194
- @inline_cache_strategy = normalized_strategy
195
- @inline_cache_ttl = cache_ttl
196
- @inline_cache_proc = normalized_proc
197
- end
198
-
199
- private
200
-
201
- # Normalize cache_strategy to handle Symbol, String, and Proc
202
- #
203
- # @param cache_strategy [Symbol, String, Proc, nil] Cache strategy
204
- # @param cache_proc [Proc, nil] Optional cache proc (ignored if cache_strategy is a Proc)
205
- # @return [Array<Symbol, Proc|nil>] Normalized strategy and proc
206
- def normalize_cache_strategy(cache_strategy, cache_proc)
207
- case cache_strategy
208
- when Proc
209
- # If cache_strategy is a Proc, use it as the proc and set strategy to :proc
210
- [:proc, cache_strategy]
211
- when String
212
- # Convert string to symbol
213
- normalized = cache_strategy.to_sym
214
- unless [:none, :time, :proc, :file].include?(normalized)
215
- raise ArgumentError, "cache_strategy must be :none, :time, :proc, or :file (got: #{cache_strategy.inspect})"
216
- end
217
- [normalized, cache_proc]
218
- when Symbol
219
- unless [:none, :time, :proc, :file].include?(cache_strategy)
220
- raise ArgumentError, "cache_strategy must be :none, :time, :proc, or :file (got: #{cache_strategy.inspect})"
221
- end
222
- [cache_strategy, cache_proc]
223
- when nil
224
- [:none, nil]
225
- else
226
- raise ArgumentError, "cache_strategy must be a Symbol, String, or Proc (got: #{cache_strategy.class})"
227
- end
228
- end
229
-
230
- # Deprecated: Use stylesheet_registry instead
231
- # @deprecated Use {#stylesheet_registry} instead
232
- def head_rendering!
233
- stylesheet_registry
234
- end
235
-
236
- # Check if component uses head rendering
237
- def head_rendering?
238
- return false unless defined?(@head_rendering)
239
- @head_rendering
240
- end
241
-
242
- public :head_rendering?
243
-
244
- # Get the namespace for stylesheet registry
245
- def stylesheet_namespace
246
- @stylesheet_namespace if defined?(@stylesheet_namespace)
247
- end
248
-
249
- # Get the custom scope ID if set (alias for capsule_id getter)
250
- def custom_capsule_id
251
- @custom_capsule_id if defined?(@custom_capsule_id)
252
- end
253
-
254
- # Get inline cache strategy
255
- def inline_cache_strategy
256
- @inline_cache_strategy if defined?(@inline_cache_strategy)
257
- end
258
-
259
- # Get inline cache TTL
260
- def inline_cache_ttl
261
- @inline_cache_ttl if defined?(@inline_cache_ttl)
262
- end
263
-
264
- # Get inline cache proc
265
- def inline_cache_proc
266
- @inline_cache_proc if defined?(@inline_cache_proc)
267
- end
268
-
269
- public :head_rendering?, :stylesheet_namespace, :custom_capsule_id, :inline_cache_strategy, :inline_cache_ttl, :inline_cache_proc
270
-
271
- # Set or get options for stylesheet_link_tag when using file-based caching
272
- #
273
- # @param options [Hash, nil] Options to pass to stylesheet_link_tag (e.g., "data-turbo-track": "reload", omit to get current value)
274
- # @return [Hash, nil] The current stylesheet link options if no argument provided
275
- # @example Setting stylesheet link options
276
- # class MyComponent < ApplicationComponent
277
- # include StyleCapsule::Component
278
- # stylesheet_registry cache_strategy: :file
279
- # stylesheet_link_options "data-turbo-track": "reload"
280
- # end
281
- # @example Getting the current options
282
- # MyComponent.stylesheet_link_options # => {"data-turbo-track" => "reload"} or nil
283
- def stylesheet_link_options(options = nil)
284
- if options.nil?
285
- @stylesheet_link_options if defined?(@stylesheet_link_options)
286
- else
287
- @stylesheet_link_options = options
288
- end
289
- end
290
-
291
- public :stylesheet_link_options
292
-
293
- # Set or get CSS scoping strategy
294
- #
295
- # @param strategy [Symbol, nil] Scoping strategy: :selector_patching (default) or :nesting (omit to get current value)
296
- # - :selector_patching: Adds [data-capsule="..."] prefix to each selector (better browser support)
297
- # - :nesting: Wraps entire CSS in [data-capsule="..."] { ... } (more performant, requires CSS nesting support)
298
- # @return [Symbol] The current scoping strategy (default: :selector_patching)
299
- # @example Using CSS nesting (requires Chrome 112+, Firefox 117+, Safari 16.5+)
300
- # class MyComponent < ApplicationComponent
301
- # include StyleCapsule::Component
302
- # css_scoping_strategy :nesting # More performant, no CSS parsing needed
303
- #
304
- # def component_styles
305
- # <<~CSS
306
- # .section { color: red; }
307
- # .heading:hover { opacity: 0.8; }
308
- # CSS
309
- # end
310
- # end
311
- # # Output: [data-capsule="abc123"] { .section { color: red; } .heading:hover { opacity: 0.8; } }
312
- # @example Using selector patching (default, better browser support)
313
- # class MyComponent < ApplicationComponent
314
- # include StyleCapsule::Component
315
- # css_scoping_strategy :selector_patching # Default
316
- #
317
- # def component_styles
318
- # <<~CSS
319
- # .section { color: red; }
320
- # CSS
321
- # end
322
- # end
323
- # # Output: [data-capsule="abc123"] .section { color: red; }
324
- def css_scoping_strategy(strategy = nil)
325
- if strategy.nil?
326
- # Check if this class has a strategy set
327
- if defined?(@css_scoping_strategy) && @css_scoping_strategy
328
- @css_scoping_strategy
329
- # Otherwise, check parent class (for inheritance)
330
- elsif superclass.respond_to?(:css_scoping_strategy, true)
331
- superclass.css_scoping_strategy
332
- else
333
- :selector_patching
334
- end
335
- else
336
- unless [:selector_patching, :nesting].include?(strategy)
337
- raise ArgumentError, "css_scoping_strategy must be :selector_patching or :nesting (got: #{strategy.inspect})"
338
- end
339
- @css_scoping_strategy = strategy
340
- end
341
- end
342
-
343
- public :css_scoping_strategy
99
+ include StyleCapsule::ComponentClassMethods
344
100
  end
345
101
 
346
102
  # Module that wraps view_template to add scoped wrapper
@@ -350,8 +106,11 @@ module StyleCapsule
350
106
  # Render styles first
351
107
  render_capsule_styles
352
108
 
109
+ # Get wrapper tag
110
+ tag = self.class.wrapper_tag
111
+
353
112
  # Wrap content in scoped element
354
- div(data_capsule: component_capsule) do
113
+ public_send(tag, data_capsule: component_capsule) do
355
114
  super
356
115
  end
357
116
  else
@@ -389,6 +148,7 @@ module StyleCapsule
389
148
  #
390
149
  # Supports both instance method (def component_styles) and class method (def self.component_styles).
391
150
  # File caching is only allowed for class method component_styles.
151
+ # rubocop:disable Metrics/AbcSize -- coordinates head vs body rendering, caching, and registry
392
152
  def render_capsule_styles
393
153
  css_content = component_styles_content
394
154
  return if css_content.nil? || css_content.to_s.strip.empty?
@@ -439,6 +199,7 @@ module StyleCapsule
439
199
  end
440
200
  end
441
201
  end
202
+ # rubocop:enable Metrics/AbcSize
442
203
 
443
204
  # Check if component should use head rendering
444
205
  #
@@ -450,11 +211,12 @@ module StyleCapsule
450
211
 
451
212
  # Scope CSS and return scoped CSS with attribute selectors
452
213
  def scope_css(css_content)
453
- # Use class-level cache to avoid reprocessing same CSS
454
- # Include capsule_id and scoping strategy in cache key
214
+ # Use class-level cache to avoid reprocessing same CSS. Include a fingerprint of the
215
+ # source CSS so instance methods that return different styles per render stay correct.
455
216
  capsule_id = component_capsule
456
217
  scoping_strategy = self.class.css_scoping_strategy
457
- cache_key = "#{self.class.name}:#{capsule_id}:#{scoping_strategy}"
218
+ css_fingerprint = Digest::SHA1.hexdigest(css_content.to_s)
219
+ cache_key = "#{self.class.name}:#{capsule_id}:#{scoping_strategy}:#{css_fingerprint}"
458
220
 
459
221
  if self.class.css_cache.key?(cache_key)
460
222
  return self.class.css_cache[cache_key]
@@ -468,10 +230,7 @@ module StyleCapsule
468
230
  CssProcessor.scope_selectors(css_content, capsule_id, component_class: self.class)
469
231
  end
470
232
 
471
- # Cache at class level (one style block per component type/scope/strategy combination)
472
- self.class.css_cache[cache_key] = scoped_css
473
-
474
- scoped_css
233
+ self.class.store_css_cache(cache_key, scoped_css)
475
234
  end
476
235
 
477
236
  # Generate a unique scope ID based on component class name (per-component-type)
@@ -85,6 +85,7 @@ module StyleCapsule
85
85
  # @param component_class [Class] Component class to build
86
86
  # @param output_proc [Proc, nil] Optional proc to call with output messages
87
87
  # @return [String, nil] Generated file path or nil if skipped
88
+ # rubocop:disable Metrics/AbcSize -- file build path with instrumentation and error handling
88
89
  def build_component(component_class, output_proc: nil)
89
90
  return nil unless component_class.inline_cache_strategy == :file
90
91
  # Check for class method component_styles (required for file caching)
@@ -111,11 +112,13 @@ module StyleCapsule
111
112
  output_proc&.call("Generated: #{file_path}") if file_path
112
113
  file_path
113
114
  rescue ArgumentError, NoMethodError => e
114
- # Component requires arguments or has dependencies - skip it
115
+ # Component requires arguments or has dependencies - skip it (common for ViewComponent with required kwargs)
116
+ warn "[style_capsule] Skipped #{component_class.name} in style_capsule:build — #{e.class}: #{e.message}"
115
117
  output_proc&.call("Skipped #{component_class.name}: #{e.message}")
116
118
  nil
117
119
  end
118
120
  end
121
+ # rubocop:enable Metrics/AbcSize
119
122
 
120
123
  # Build CSS files for all components
121
124
  #
@@ -0,0 +1,295 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StyleCapsule
4
+ # Shared class-level DSL for {StyleCapsule::Component} and {StyleCapsule::ViewComponent}.
5
+ #
6
+ # @api private
7
+ module ComponentClassMethods
8
+ MAX_CSS_CACHE_ENTRIES = 256
9
+
10
+ # Class-level cache for scoped CSS per component class
11
+ def css_cache
12
+ @css_cache ||= {}
13
+ end
14
+
15
+ # Store scoped CSS in the bounded class-level cache
16
+ #
17
+ # @param cache_key [String]
18
+ # @param scoped_css [String]
19
+ # @return [String] +scoped_css+
20
+ def store_css_cache(cache_key, scoped_css)
21
+ cache = css_cache
22
+ order = css_cache_order
23
+ evict_css_cache_if_full!(cache, order)
24
+ cache[cache_key] = scoped_css
25
+ order << cache_key
26
+ scoped_css
27
+ end
28
+
29
+ # Clear the CSS cache for this component class
30
+ #
31
+ # Useful for testing or when you want to force CSS reprocessing.
32
+ # In development, this is automatically called when classes are reloaded.
33
+ #
34
+ # @example
35
+ # MyComponent.clear_css_cache
36
+ def clear_css_cache
37
+ @css_cache = {}
38
+ @css_cache_order = []
39
+ end
40
+
41
+ private
42
+
43
+ def css_cache_order
44
+ @css_cache_order ||= []
45
+ end
46
+
47
+ def evict_css_cache_if_full!(cache, order)
48
+ while order.size >= MAX_CSS_CACHE_ENTRIES
49
+ old = order.shift
50
+ cache.delete(old)
51
+ end
52
+ end
53
+
54
+ public
55
+
56
+ # Set or get a custom capsule ID for this component class (useful for testing)
57
+ #
58
+ # @param capsule_id [String, nil] The custom capsule ID to use (nil to get current value)
59
+ # @return [String, nil] The current capsule ID if no argument provided
60
+ # @example Setting a custom capsule ID
61
+ # class MyComponent < ApplicationComponent
62
+ # include StyleCapsule::Component
63
+ # capsule_id "test-capsule-123"
64
+ # end
65
+ # @example Getting the current capsule ID
66
+ # MyComponent.capsule_id # => "test-capsule-123" or nil
67
+ def capsule_id(capsule_id = nil)
68
+ if capsule_id.nil?
69
+ @custom_capsule_id if defined?(@custom_capsule_id)
70
+ else
71
+ @custom_capsule_id = capsule_id.to_s
72
+ end
73
+ end
74
+
75
+ # Configure stylesheet registry for head rendering
76
+ #
77
+ # Enables head rendering and configures namespace and cache strategy in a single call.
78
+ # All parameters are optional - calling without arguments enables head rendering with defaults.
79
+ #
80
+ # @deprecated Prefer {#style_capsule}, which configures namespace, cache, scoping, and head
81
+ # rendering in one place. This method remains for backward compatibility.
82
+ #
83
+ # @param namespace [Symbol, String, nil] Namespace identifier (nil/blank uses default)
84
+ # @param cache_strategy [Symbol, String, Proc, nil] Cache strategy: :none (default), :time, :proc, :file
85
+ # - Symbol or String: :none, :time, :proc, :file (or "none", "time", "proc", "file")
86
+ # - Proc: Custom cache proc (automatically uses :proc strategy)
87
+ # Proc receives: (css_content, capsule_id, namespace) and should return [cache_key, should_cache, expires_at]
88
+ # @param cache_ttl [Integer, ActiveSupport::Duration, nil] Time-to-live in seconds (for :time strategy). Supports ActiveSupport::Duration (e.g., 1.hour, 30.minutes)
89
+ # @param cache_proc [Proc, nil] Custom cache proc (for :proc strategy, ignored if cache_strategy is a Proc)
90
+ # Proc receives: (css_content, capsule_id, namespace) and should return [cache_key, should_cache, expires_at]
91
+ # @return [void]
92
+ def stylesheet_registry(namespace: nil, cache_strategy: :none, cache_ttl: nil, cache_proc: nil)
93
+ @head_rendering = true
94
+ @stylesheet_namespace = namespace unless namespace.nil?
95
+
96
+ # Normalize cache_strategy: convert strings to symbols, handle Proc
97
+ normalized_strategy, normalized_proc = normalize_cache_strategy(cache_strategy, cache_proc)
98
+ @inline_cache_strategy = normalized_strategy
99
+ @inline_cache_ttl = cache_ttl
100
+ @inline_cache_proc = normalized_proc
101
+ end
102
+
103
+ private
104
+
105
+ # Normalize cache_strategy to handle Symbol, String, and Proc
106
+ #
107
+ # @param cache_strategy [Symbol, String, Proc, nil] Cache strategy
108
+ # @param cache_proc [Proc, nil] Optional cache proc (ignored if cache_strategy is a Proc)
109
+ # @return [Array<Symbol, Proc|nil>] Normalized strategy and proc
110
+ def normalize_cache_strategy(cache_strategy, cache_proc)
111
+ case cache_strategy
112
+ when Proc
113
+ # If cache_strategy is a Proc, use it as the proc and set strategy to :proc
114
+ [:proc, cache_strategy]
115
+ when String
116
+ # Convert string to symbol
117
+ normalized = cache_strategy.to_sym
118
+ unless [:none, :time, :proc, :file].include?(normalized)
119
+ raise ArgumentError, "cache_strategy must be :none, :time, :proc, or :file (got: #{cache_strategy.inspect})"
120
+ end
121
+ [normalized, cache_proc]
122
+ when Symbol
123
+ unless [:none, :time, :proc, :file].include?(cache_strategy)
124
+ raise ArgumentError, "cache_strategy must be :none, :time, :proc, or :file (got: #{cache_strategy.inspect})"
125
+ end
126
+ [cache_strategy, cache_proc]
127
+ when nil
128
+ [:none, nil]
129
+ else
130
+ raise ArgumentError, "cache_strategy must be a Symbol, String, or Proc (got: #{cache_strategy.class})"
131
+ end
132
+ end
133
+
134
+ # Check if component uses head rendering (checks instance variable, then parent class, defaults to false)
135
+ #
136
+ # @return [Boolean] Whether head rendering is enabled (default: false)
137
+ def head_rendering?
138
+ if defined?(@head_rendering)
139
+ @head_rendering
140
+ elsif superclass.respond_to?(:head_rendering?, true)
141
+ superclass.head_rendering?
142
+ else
143
+ false
144
+ end
145
+ end
146
+
147
+ public :head_rendering?
148
+
149
+ # Get the namespace for stylesheet registry (checks instance variable, then parent class, defaults to nil)
150
+ #
151
+ # @return [Symbol, String, nil] The namespace identifier (default: nil)
152
+ def stylesheet_namespace
153
+ if defined?(@stylesheet_namespace) && @stylesheet_namespace
154
+ @stylesheet_namespace
155
+ elsif superclass.respond_to?(:stylesheet_namespace, true)
156
+ superclass.stylesheet_namespace
157
+ end
158
+ end
159
+
160
+ # Configure StyleCapsule settings
161
+ #
162
+ # All settings support class inheritance - child classes inherit settings from parent classes
163
+ # and can override them by calling style_capsule again with different values.
164
+ #
165
+ # @param namespace [Symbol, String, nil] Default namespace for stylesheets
166
+ # @param cache_strategy [Symbol, String, Proc, nil] Cache strategy: :none (default), :time, :proc, :file
167
+ # @param cache_ttl [Integer, ActiveSupport::Duration, nil] Time-to-live in seconds (for :time strategy)
168
+ # @param cache_proc [Proc, nil] Custom cache proc (for :proc strategy)
169
+ # @param scoping_strategy [Symbol, nil] CSS scoping strategy: :selector_patching (default) or :nesting
170
+ # @param head_rendering [Boolean, nil] Enable head rendering (default: true if any option is set, false otherwise)
171
+ # @param tag [Symbol, String, nil] HTML tag name for wrapper element (default: :div)
172
+ # @return [void]
173
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- DSL configures multiple optional class settings
174
+ def style_capsule(namespace: nil, cache_strategy: nil, cache_ttl: nil, cache_proc: nil, scoping_strategy: nil, head_rendering: nil, tag: nil)
175
+ # Set namespace (stored in instance variable, but getter checks parent class for inheritance)
176
+ if namespace
177
+ @stylesheet_namespace = namespace
178
+ end
179
+
180
+ # Configure cache strategy if provided
181
+ if cache_strategy || cache_ttl || cache_proc
182
+ normalized_strategy, normalized_proc = normalize_cache_strategy(cache_strategy || :none, cache_proc)
183
+ @inline_cache_strategy = normalized_strategy
184
+ # Explicitly set cache_ttl (even if nil) to override parent's value when cache settings are changed
185
+ @inline_cache_ttl = cache_ttl
186
+ @inline_cache_proc = normalized_proc
187
+ end
188
+
189
+ # Configure CSS scoping strategy if provided
190
+ if scoping_strategy
191
+ unless [:selector_patching, :nesting].include?(scoping_strategy)
192
+ raise ArgumentError, "scoping_strategy must be :selector_patching or :nesting (got: #{scoping_strategy.inspect})"
193
+ end
194
+ @css_scoping_strategy = scoping_strategy
195
+ end
196
+
197
+ # Configure wrapper tag if provided
198
+ if tag
199
+ @wrapper_tag = tag
200
+ end
201
+
202
+ # Enable head rendering if explicitly set or if any option is provided (except scoping_strategy)
203
+ if head_rendering.nil?
204
+ @head_rendering = true if namespace || cache_strategy || cache_ttl || cache_proc
205
+ else
206
+ @head_rendering = head_rendering
207
+ end
208
+ end
209
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
210
+
211
+ # Get the custom scope ID if set (alias for capsule_id getter)
212
+ def custom_capsule_id
213
+ @custom_capsule_id if defined?(@custom_capsule_id)
214
+ end
215
+
216
+ # Get inline cache strategy (checks instance variable, then parent class, defaults to nil)
217
+ #
218
+ # @return [Symbol, nil] The cache strategy (default: nil)
219
+ def inline_cache_strategy
220
+ if defined?(@inline_cache_strategy) && @inline_cache_strategy
221
+ @inline_cache_strategy
222
+ elsif superclass.respond_to?(:inline_cache_strategy, true)
223
+ superclass.inline_cache_strategy
224
+ end
225
+ end
226
+
227
+ # Get inline cache TTL (checks instance variable, then parent class, defaults to nil)
228
+ #
229
+ # @return [Integer, ActiveSupport::Duration, nil] The cache TTL (default: nil)
230
+ def inline_cache_ttl
231
+ if defined?(@inline_cache_ttl)
232
+ @inline_cache_ttl
233
+ elsif superclass.respond_to?(:inline_cache_ttl, true)
234
+ superclass.inline_cache_ttl
235
+ end
236
+ end
237
+
238
+ # Get inline cache proc (checks instance variable, then parent class, defaults to nil)
239
+ #
240
+ # @return [Proc, nil] The cache proc (default: nil)
241
+ def inline_cache_proc
242
+ if defined?(@inline_cache_proc)
243
+ @inline_cache_proc
244
+ elsif superclass.respond_to?(:inline_cache_proc, true)
245
+ superclass.inline_cache_proc
246
+ end
247
+ end
248
+
249
+ public :head_rendering?, :stylesheet_namespace, :style_capsule, :custom_capsule_id, :inline_cache_strategy, :inline_cache_ttl, :inline_cache_proc
250
+
251
+ # Get CSS scoping strategy (checks instance variable, then parent class, defaults to :selector_patching)
252
+ #
253
+ # @return [Symbol] The current scoping strategy (default: :selector_patching)
254
+ def css_scoping_strategy
255
+ if defined?(@css_scoping_strategy) && @css_scoping_strategy
256
+ @css_scoping_strategy
257
+ elsif superclass.respond_to?(:css_scoping_strategy, true)
258
+ superclass.css_scoping_strategy
259
+ else
260
+ :selector_patching
261
+ end
262
+ end
263
+
264
+ public :css_scoping_strategy
265
+
266
+ # Get wrapper tag (checks instance variable, then parent class, defaults to :div)
267
+ #
268
+ # @return [Symbol, String] The wrapper tag (default: :div)
269
+ def wrapper_tag
270
+ if defined?(@wrapper_tag) && @wrapper_tag
271
+ @wrapper_tag
272
+ elsif superclass.respond_to?(:wrapper_tag, true)
273
+ superclass.wrapper_tag
274
+ else
275
+ :div
276
+ end
277
+ end
278
+
279
+ public :wrapper_tag
280
+
281
+ # Set or get options for stylesheet_link_tag when using file-based caching
282
+ #
283
+ # @param options [Hash, nil] Options to pass to stylesheet_link_tag (e.g., "data-turbo-track": "reload", omit to get current value)
284
+ # @return [Hash, nil] The current stylesheet link options if no argument provided
285
+ def stylesheet_link_options(options = nil)
286
+ if options.nil?
287
+ @stylesheet_link_options if defined?(@stylesheet_link_options)
288
+ else
289
+ @stylesheet_link_options = options
290
+ end
291
+ end
292
+
293
+ public :stylesheet_link_options
294
+ end
295
+ end