style_capsule 1.4.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,314 +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
- # Check if component uses head rendering (checks instance variable, then parent class, defaults to false)
231
- #
232
- # @return [Boolean] Whether head rendering is enabled (default: false)
233
- def head_rendering?
234
- if defined?(@head_rendering)
235
- @head_rendering
236
- elsif superclass.respond_to?(:head_rendering?, true)
237
- superclass.head_rendering?
238
- else
239
- false
240
- end
241
- end
242
-
243
- public :head_rendering?
244
-
245
- # Get the namespace for stylesheet registry (checks instance variable, then parent class, defaults to nil)
246
- #
247
- # @return [Symbol, String, nil] The namespace identifier (default: nil)
248
- def stylesheet_namespace
249
- if defined?(@stylesheet_namespace) && @stylesheet_namespace
250
- @stylesheet_namespace
251
- elsif superclass.respond_to?(:stylesheet_namespace, true)
252
- superclass.stylesheet_namespace
253
- end
254
- end
255
-
256
- # Configure StyleCapsule settings
257
- #
258
- # All settings support class inheritance - child classes inherit settings from parent classes
259
- # and can override them by calling style_capsule again with different values.
260
- #
261
- # @param namespace [Symbol, String, nil] Default namespace for stylesheets
262
- # @param cache_strategy [Symbol, String, Proc, nil] Cache strategy: :none (default), :time, :proc, :file
263
- # @param cache_ttl [Integer, ActiveSupport::Duration, nil] Time-to-live in seconds (for :time strategy)
264
- # @param cache_proc [Proc, nil] Custom cache proc (for :proc strategy)
265
- # @param scoping_strategy [Symbol, nil] CSS scoping strategy: :selector_patching (default) or :nesting
266
- # @param head_rendering [Boolean, nil] Enable head rendering (default: true if any option is set, false otherwise)
267
- # @return [void]
268
- # @example Basic usage with namespace
269
- # class AdminComponent < ApplicationComponent
270
- # include StyleCapsule::Component
271
- # style_capsule namespace: :admin
272
- #
273
- # def view_template
274
- # register_stylesheet("stylesheets/admin/dashboard") # Uses :admin namespace automatically
275
- # div { "Content" }
276
- # end
277
- # end
278
- # @example With all options
279
- # class MyComponent < ApplicationComponent
280
- # include StyleCapsule::Component
281
- # style_capsule(
282
- # namespace: :user,
283
- # cache_strategy: :time,
284
- # cache_ttl: 1.hour,
285
- # scoping_strategy: :nesting
286
- # )
287
- # end
288
- # @example Inheritance - child class inherits parent settings
289
- # class BaseComponent < ApplicationComponent
290
- # include StyleCapsule::Component
291
- # style_capsule namespace: :admin, cache_strategy: :time, cache_ttl: 1.hour
292
- # end
293
- #
294
- # class ChildComponent < BaseComponent
295
- # # Inherits namespace: :admin, cache_strategy: :time, cache_ttl: 1.hour
296
- # # Can override specific settings:
297
- # style_capsule namespace: :user # Overrides namespace, keeps cache settings
298
- # end
299
- def style_capsule(namespace: nil, cache_strategy: nil, cache_ttl: nil, cache_proc: nil, scoping_strategy: nil, head_rendering: nil)
300
- # Set namespace (stored in instance variable, but getter checks parent class for inheritance)
301
- if namespace
302
- @stylesheet_namespace = namespace
303
- end
304
-
305
- # Configure cache strategy if provided
306
- if cache_strategy || cache_ttl || cache_proc
307
- normalized_strategy, normalized_proc = normalize_cache_strategy(cache_strategy || :none, cache_proc)
308
- @inline_cache_strategy = normalized_strategy
309
- # Explicitly set cache_ttl (even if nil) to override parent's value when cache settings are changed
310
- @inline_cache_ttl = cache_ttl
311
- @inline_cache_proc = normalized_proc
312
- end
313
-
314
- # Configure CSS scoping strategy if provided
315
- if scoping_strategy
316
- unless [:selector_patching, :nesting].include?(scoping_strategy)
317
- raise ArgumentError, "scoping_strategy must be :selector_patching or :nesting (got: #{scoping_strategy.inspect})"
318
- end
319
- @css_scoping_strategy = scoping_strategy
320
- end
321
-
322
- # Enable head rendering if explicitly set or if any option is provided (except scoping_strategy)
323
- if head_rendering.nil?
324
- @head_rendering = true if namespace || cache_strategy || cache_ttl || cache_proc
325
- else
326
- @head_rendering = head_rendering
327
- end
328
- end
329
-
330
- # Get the custom scope ID if set (alias for capsule_id getter)
331
- def custom_capsule_id
332
- @custom_capsule_id if defined?(@custom_capsule_id)
333
- end
334
-
335
- # Get inline cache strategy (checks instance variable, then parent class, defaults to nil)
336
- #
337
- # @return [Symbol, nil] The cache strategy (default: nil)
338
- def inline_cache_strategy
339
- if defined?(@inline_cache_strategy) && @inline_cache_strategy
340
- @inline_cache_strategy
341
- elsif superclass.respond_to?(:inline_cache_strategy, true)
342
- superclass.inline_cache_strategy
343
- end
344
- end
345
-
346
- # Get inline cache TTL (checks instance variable, then parent class, defaults to nil)
347
- #
348
- # @return [Integer, ActiveSupport::Duration, nil] The cache TTL (default: nil)
349
- def inline_cache_ttl
350
- if defined?(@inline_cache_ttl)
351
- @inline_cache_ttl
352
- elsif superclass.respond_to?(:inline_cache_ttl, true)
353
- superclass.inline_cache_ttl
354
- end
355
- end
356
-
357
- # Get inline cache proc (checks instance variable, then parent class, defaults to nil)
358
- #
359
- # @return [Proc, nil] The cache proc (default: nil)
360
- def inline_cache_proc
361
- if defined?(@inline_cache_proc)
362
- @inline_cache_proc
363
- elsif superclass.respond_to?(:inline_cache_proc, true)
364
- superclass.inline_cache_proc
365
- end
366
- end
367
-
368
- public :head_rendering?, :stylesheet_namespace, :style_capsule, :custom_capsule_id, :inline_cache_strategy, :inline_cache_ttl, :inline_cache_proc
369
-
370
- # Get CSS scoping strategy (checks instance variable, then parent class, defaults to :selector_patching)
371
- #
372
- # @return [Symbol] The current scoping strategy (default: :selector_patching)
373
- def css_scoping_strategy
374
- if defined?(@css_scoping_strategy) && @css_scoping_strategy
375
- @css_scoping_strategy
376
- elsif superclass.respond_to?(:css_scoping_strategy, true)
377
- superclass.css_scoping_strategy
378
- else
379
- :selector_patching
380
- end
381
- end
382
-
383
- public :css_scoping_strategy
384
-
385
- # Set or get options for stylesheet_link_tag when using file-based caching
386
- #
387
- # @param options [Hash, nil] Options to pass to stylesheet_link_tag (e.g., "data-turbo-track": "reload", omit to get current value)
388
- # @return [Hash, nil] The current stylesheet link options if no argument provided
389
- # @example Setting stylesheet link options
390
- # class MyComponent < ApplicationComponent
391
- # include StyleCapsule::Component
392
- # stylesheet_registry cache_strategy: :file
393
- # stylesheet_link_options "data-turbo-track": "reload"
394
- # end
395
- # @example Getting the current options
396
- # MyComponent.stylesheet_link_options # => {"data-turbo-track" => "reload"} or nil
397
- def stylesheet_link_options(options = nil)
398
- if options.nil?
399
- @stylesheet_link_options if defined?(@stylesheet_link_options)
400
- else
401
- @stylesheet_link_options = options
402
- end
403
- end
404
-
405
- public :stylesheet_link_options
99
+ include StyleCapsule::ComponentClassMethods
406
100
  end
407
101
 
408
102
  # Module that wraps view_template to add scoped wrapper
@@ -412,8 +106,11 @@ module StyleCapsule
412
106
  # Render styles first
413
107
  render_capsule_styles
414
108
 
109
+ # Get wrapper tag
110
+ tag = self.class.wrapper_tag
111
+
415
112
  # Wrap content in scoped element
416
- div(data_capsule: component_capsule) do
113
+ public_send(tag, data_capsule: component_capsule) do
417
114
  super
418
115
  end
419
116
  else
@@ -451,6 +148,7 @@ module StyleCapsule
451
148
  #
452
149
  # Supports both instance method (def component_styles) and class method (def self.component_styles).
453
150
  # File caching is only allowed for class method component_styles.
151
+ # rubocop:disable Metrics/AbcSize -- coordinates head vs body rendering, caching, and registry
454
152
  def render_capsule_styles
455
153
  css_content = component_styles_content
456
154
  return if css_content.nil? || css_content.to_s.strip.empty?
@@ -501,6 +199,7 @@ module StyleCapsule
501
199
  end
502
200
  end
503
201
  end
202
+ # rubocop:enable Metrics/AbcSize
504
203
 
505
204
  # Check if component should use head rendering
506
205
  #
@@ -512,11 +211,12 @@ module StyleCapsule
512
211
 
513
212
  # Scope CSS and return scoped CSS with attribute selectors
514
213
  def scope_css(css_content)
515
- # Use class-level cache to avoid reprocessing same CSS
516
- # 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.
517
216
  capsule_id = component_capsule
518
217
  scoping_strategy = self.class.css_scoping_strategy
519
- 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}"
520
220
 
521
221
  if self.class.css_cache.key?(cache_key)
522
222
  return self.class.css_cache[cache_key]
@@ -530,10 +230,7 @@ module StyleCapsule
530
230
  CssProcessor.scope_selectors(css_content, capsule_id, component_class: self.class)
531
231
  end
532
232
 
533
- # Cache at class level (one style block per component type/scope/strategy combination)
534
- self.class.css_cache[cache_key] = scoped_css
535
-
536
- scoped_css
233
+ self.class.store_css_cache(cache_key, scoped_css)
537
234
  end
538
235
 
539
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