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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -1
- data/README.md +92 -105
- data/docs/non_rails_support.md +85 -0
- data/lib/style_capsule/asset_path.rb +39 -0
- data/lib/style_capsule/class_registry.rb +3 -2
- data/lib/style_capsule/component.rb +13 -254
- data/lib/style_capsule/component_builder.rb +4 -1
- data/lib/style_capsule/component_class_methods.rb +295 -0
- data/lib/style_capsule/css_file_writer.rb +62 -25
- data/lib/style_capsule/css_processor.rb +4 -6
- data/lib/style_capsule/head_injection_middleware.rb +72 -0
- data/lib/style_capsule/helper.rb +17 -27
- data/lib/style_capsule/helper_scope_cache.rb +72 -0
- data/lib/style_capsule/instrumentation.rb +15 -15
- data/lib/style_capsule/phlex_helper.rb +19 -16
- data/lib/style_capsule/railtie.rb +11 -0
- data/lib/style_capsule/standalone_helper.rb +12 -23
- data/lib/style_capsule/stylesheet_registry.rb +244 -64
- data/lib/style_capsule/version.rb +1 -1
- data/lib/style_capsule/view_component.rb +13 -256
- data/lib/style_capsule/view_component_helper.rb +16 -9
- data/lib/style_capsule.rb +14 -5
- data/lib/tasks/style_capsule.rake +15 -2
- data/sig/style_capsule.rbs +43 -44
- metadata +84 -9
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|