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::ViewComponent
|
|
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::ViewComponent
|
|
150
|
-
# stylesheet_registry
|
|
151
|
-
# end
|
|
152
|
-
# @example With namespace
|
|
153
|
-
# class AdminComponent < ApplicationComponent
|
|
154
|
-
# include StyleCapsule::ViewComponent
|
|
155
|
-
# stylesheet_registry namespace: :admin
|
|
156
|
-
# end
|
|
157
|
-
# @example With time-based caching
|
|
158
|
-
# class MyComponent < ApplicationComponent
|
|
159
|
-
# include StyleCapsule::ViewComponent
|
|
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::ViewComponent
|
|
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::ViewComponent
|
|
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::ViewComponent
|
|
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::ViewComponent
|
|
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::ViewComponent
|
|
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::ViewComponent
|
|
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 call to add scoped wrapper
|
|
@@ -353,8 +109,11 @@ module StyleCapsule
|
|
|
353
109
|
# Get content from original call method
|
|
354
110
|
content_html = super
|
|
355
111
|
|
|
112
|
+
# Get wrapper tag
|
|
113
|
+
tag = self.class.wrapper_tag
|
|
114
|
+
|
|
356
115
|
# Wrap content in scoped element
|
|
357
|
-
scoped_wrapper = helpers.content_tag(
|
|
116
|
+
scoped_wrapper = helpers.content_tag(tag, content_html.html_safe, data: {capsule: component_capsule})
|
|
358
117
|
|
|
359
118
|
# Combine styles and wrapped content
|
|
360
119
|
(styles_html + scoped_wrapper).html_safe
|
|
@@ -388,6 +147,7 @@ module StyleCapsule
|
|
|
388
147
|
# File caching is only allowed for class method component_styles.
|
|
389
148
|
#
|
|
390
149
|
# @return [String] HTML string with style tag or empty string
|
|
150
|
+
# rubocop:disable Metrics/AbcSize -- coordinates head vs body rendering, caching, and registry
|
|
391
151
|
def render_capsule_styles
|
|
392
152
|
css_content = component_styles_content
|
|
393
153
|
return "".html_safe if css_content.nil? || css_content.to_s.strip.empty?
|
|
@@ -433,6 +193,7 @@ module StyleCapsule
|
|
|
433
193
|
helpers.content_tag(:style, scoped_css.html_safe, type: "text/css")
|
|
434
194
|
end
|
|
435
195
|
end
|
|
196
|
+
# rubocop:enable Metrics/AbcSize
|
|
436
197
|
|
|
437
198
|
# Check if component should use head rendering
|
|
438
199
|
#
|
|
@@ -444,11 +205,10 @@ module StyleCapsule
|
|
|
444
205
|
|
|
445
206
|
# Scope CSS and return scoped CSS with attribute selectors
|
|
446
207
|
def scope_css(css_content)
|
|
447
|
-
# Use class-level cache to avoid reprocessing same CSS
|
|
448
|
-
# Include capsule_id and scoping strategy in cache key
|
|
449
208
|
capsule_id = component_capsule
|
|
450
209
|
scoping_strategy = self.class.css_scoping_strategy
|
|
451
|
-
|
|
210
|
+
css_fingerprint = Digest::SHA1.hexdigest(css_content.to_s)
|
|
211
|
+
cache_key = "#{self.class.name}:#{capsule_id}:#{scoping_strategy}:#{css_fingerprint}"
|
|
452
212
|
|
|
453
213
|
if self.class.css_cache.key?(cache_key)
|
|
454
214
|
return self.class.css_cache[cache_key]
|
|
@@ -457,15 +217,12 @@ module StyleCapsule
|
|
|
457
217
|
# Use the configured scoping strategy
|
|
458
218
|
scoped_css = case scoping_strategy
|
|
459
219
|
when :nesting
|
|
460
|
-
CssProcessor.scope_with_nesting(css_content, capsule_id)
|
|
220
|
+
CssProcessor.scope_with_nesting(css_content, capsule_id, component_class: self.class)
|
|
461
221
|
else # :selector_patching (default)
|
|
462
|
-
CssProcessor.scope_selectors(css_content, capsule_id)
|
|
222
|
+
CssProcessor.scope_selectors(css_content, capsule_id, component_class: self.class)
|
|
463
223
|
end
|
|
464
224
|
|
|
465
|
-
|
|
466
|
-
self.class.css_cache[cache_key] = scoped_css
|
|
467
|
-
|
|
468
|
-
scoped_css
|
|
225
|
+
self.class.store_css_cache(cache_key, scoped_css)
|
|
469
226
|
end
|
|
470
227
|
|
|
471
228
|
# Generate a unique capsule ID based on component class name (per-component-type)
|
|
@@ -10,13 +10,17 @@ module StyleCapsule
|
|
|
10
10
|
#
|
|
11
11
|
# Usage in ViewComponent layouts:
|
|
12
12
|
# def call
|
|
13
|
-
# helpers.
|
|
13
|
+
# helpers.stylesheet_registry_tags
|
|
14
14
|
# end
|
|
15
15
|
#
|
|
16
16
|
# Usage in ViewComponent components:
|
|
17
|
-
#
|
|
18
|
-
# register_stylesheet
|
|
19
|
-
#
|
|
17
|
+
# class MyComponent < ApplicationComponent
|
|
18
|
+
# style_capsule namespace: :user # Set default namespace for register_stylesheet
|
|
19
|
+
#
|
|
20
|
+
# def call
|
|
21
|
+
# register_stylesheet("stylesheets/user/my_component") # Uses :user namespace automatically
|
|
22
|
+
# content_tag(:div, "Content")
|
|
23
|
+
# end
|
|
20
24
|
# end
|
|
21
25
|
module ViewComponentHelper
|
|
22
26
|
# Register a stylesheet file for head rendering
|
|
@@ -28,11 +32,18 @@ module StyleCapsule
|
|
|
28
32
|
# content_tag(:div, "Content")
|
|
29
33
|
# end
|
|
30
34
|
#
|
|
35
|
+
# If the component has a default namespace set via style_capsule or stylesheet_registry,
|
|
36
|
+
# it will be used automatically when namespace is not explicitly provided.
|
|
37
|
+
#
|
|
31
38
|
# @param file_path [String] Path to stylesheet (relative to app/assets/stylesheets)
|
|
32
|
-
# @param namespace [Symbol, String, nil] Optional namespace for separation (nil/blank uses default)
|
|
39
|
+
# @param namespace [Symbol, String, nil] Optional namespace for separation (nil/blank uses component's default or global default)
|
|
33
40
|
# @param options [Hash] Options for stylesheet_link_tag
|
|
34
41
|
# @return [void]
|
|
35
42
|
def register_stylesheet(file_path, namespace: nil, **options)
|
|
43
|
+
# Use component's default namespace if not explicitly provided
|
|
44
|
+
if namespace.nil? && respond_to?(:class) && self.class.respond_to?(:stylesheet_namespace)
|
|
45
|
+
namespace = self.class.stylesheet_namespace
|
|
46
|
+
end
|
|
36
47
|
StyleCapsule::StylesheetRegistry.register(file_path, namespace: namespace, **options)
|
|
37
48
|
end
|
|
38
49
|
|
|
@@ -49,9 +60,5 @@ module StyleCapsule
|
|
|
49
60
|
def stylesheet_registry_tags(namespace: nil)
|
|
50
61
|
StyleCapsule::StylesheetRegistry.render_head_stylesheets(helpers, namespace: namespace)
|
|
51
62
|
end
|
|
52
|
-
|
|
53
|
-
# @deprecated Use {#stylesheet_registry_tags} instead.
|
|
54
|
-
# This method name will be removed in a future version.
|
|
55
|
-
alias_method :stylesheet_registrymap_tags, :stylesheet_registry_tags
|
|
56
63
|
end
|
|
57
64
|
end
|
data/lib/style_capsule.rb
CHANGED
|
@@ -18,6 +18,7 @@ end
|
|
|
18
18
|
# @example Phlex Component Usage
|
|
19
19
|
# class MyComponent < ApplicationComponent
|
|
20
20
|
# include StyleCapsule::Component
|
|
21
|
+
# style_capsule namespace: :user # Configure namespace and other settings
|
|
21
22
|
#
|
|
22
23
|
# def component_styles
|
|
23
24
|
# <<~CSS
|
|
@@ -29,6 +30,7 @@ end
|
|
|
29
30
|
# @example ViewComponent Encapsulation Usage
|
|
30
31
|
# class MyComponent < ApplicationComponent
|
|
31
32
|
# include StyleCapsule::ViewComponent
|
|
33
|
+
# style_capsule namespace: :admin # Configure namespace and other settings
|
|
32
34
|
#
|
|
33
35
|
# def component_styles
|
|
34
36
|
# <<~CSS
|
|
@@ -51,8 +53,10 @@ end
|
|
|
51
53
|
# end
|
|
52
54
|
#
|
|
53
55
|
# class MyComponent < ApplicationComponent
|
|
56
|
+
# style_capsule namespace: :user # Configure namespace
|
|
57
|
+
#
|
|
54
58
|
# def call
|
|
55
|
-
# register_stylesheet("stylesheets/user/my_component")
|
|
59
|
+
# register_stylesheet("stylesheets/user/my_component") # Uses :user namespace automatically
|
|
56
60
|
# content_tag(:div, "Content", class: "section")
|
|
57
61
|
# end
|
|
58
62
|
# end
|
|
@@ -68,9 +72,12 @@ end
|
|
|
68
72
|
# <%= stylesheet_registry_tags(namespace: :admin) %>
|
|
69
73
|
#
|
|
70
74
|
# @example Namespace Support
|
|
71
|
-
# #
|
|
72
|
-
# StyleCapsule::StylesheetRegistry.
|
|
73
|
-
# StyleCapsule::StylesheetRegistry.
|
|
75
|
+
# # Eager registrations (boot / class load; process-wide manifest)
|
|
76
|
+
# StyleCapsule::StylesheetRegistry.register_eager('stylesheets/admin/dashboard', namespace: :admin)
|
|
77
|
+
# StyleCapsule::StylesheetRegistry.register_eager('stylesheets/user/profile', namespace: :user)
|
|
78
|
+
#
|
|
79
|
+
# # Render-time registrations (request-scoped; also picked up by HeadInjectionMiddleware)
|
|
80
|
+
# StyleCapsule::StylesheetRegistry.register('stylesheets/page', namespace: :user)
|
|
74
81
|
#
|
|
75
82
|
# # Render all namespaces (default)
|
|
76
83
|
# <%= stylesheet_registry_tags %>
|
|
@@ -81,7 +88,7 @@ end
|
|
|
81
88
|
# @example File-Based Caching (HTTP Caching)
|
|
82
89
|
# class MyComponent < ApplicationComponent
|
|
83
90
|
# include StyleCapsule::Component
|
|
84
|
-
#
|
|
91
|
+
# style_capsule cache_strategy: :file # Writes CSS to files for HTTP caching
|
|
85
92
|
# end
|
|
86
93
|
#
|
|
87
94
|
# # CSS files are written to app/assets/builds/capsules/
|
|
@@ -90,6 +97,8 @@ end
|
|
|
90
97
|
module StyleCapsule
|
|
91
98
|
require_relative "style_capsule/version"
|
|
92
99
|
require_relative "style_capsule/instrumentation"
|
|
100
|
+
require_relative "style_capsule/asset_path"
|
|
101
|
+
require_relative "style_capsule/helper_scope_cache"
|
|
93
102
|
require_relative "style_capsule/css_processor"
|
|
94
103
|
require_relative "style_capsule/css_file_writer"
|
|
95
104
|
require_relative "style_capsule/stylesheet_registry"
|
|
@@ -16,7 +16,20 @@ namespace :style_capsule do
|
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
# Hook into Rails asset precompilation (similar to Tailwind CSS)
|
|
19
|
+
# Hook into Rails asset precompilation (similar to Tailwind CSS).
|
|
20
|
+
# Set `config.style_capsule.run_on_precompile = false` to skip.
|
|
21
|
+
task "style_capsule:precompile_hook" => :environment do
|
|
22
|
+
cfg = Rails.application.config
|
|
23
|
+
run = if cfg.respond_to?(:style_capsule) && cfg.style_capsule
|
|
24
|
+
cfg.style_capsule.run_on_precompile != false
|
|
25
|
+
else
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
next unless run
|
|
29
|
+
|
|
30
|
+
Rake::Task["style_capsule:build"].invoke
|
|
31
|
+
end
|
|
32
|
+
|
|
20
33
|
if defined?(Rails)
|
|
21
|
-
Rake::Task["assets:precompile"].enhance(["style_capsule:
|
|
34
|
+
Rake::Task["assets:precompile"].enhance(["style_capsule:precompile_hook"]) if Rake::Task.task_defined?("assets:precompile")
|
|
22
35
|
end
|
data/sig/style_capsule.rbs
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
module StyleCapsule
|
|
2
2
|
VERSION: String
|
|
3
3
|
|
|
4
|
+
module AssetPath
|
|
5
|
+
def self.validate_logical_path!: (String path) -> String
|
|
6
|
+
end
|
|
7
|
+
|
|
4
8
|
module CssProcessor
|
|
5
|
-
def self.scope_selectors: (String css_string, String capsule_id) -> String
|
|
6
|
-
def self.scope_with_nesting: (String css_string, String capsule_id) -> String
|
|
9
|
+
def self.scope_selectors: (String css_string, String capsule_id, ?component_class: Class | String | nil) -> String
|
|
10
|
+
def self.scope_with_nesting: (String css_string, String capsule_id, ?component_class: Class | String | nil) -> String
|
|
7
11
|
def self.strip_comments: (String css) -> String
|
|
8
12
|
end
|
|
9
13
|
|
|
@@ -17,44 +21,59 @@ module StyleCapsule
|
|
|
17
21
|
def self.enabled?: () -> bool
|
|
18
22
|
end
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
# At runtime inherits Object when ActiveSupport::CurrentAttributes is unavailable.
|
|
25
|
+
class StylesheetRegistry
|
|
21
26
|
DEFAULT_NAMESPACE: Symbol
|
|
22
27
|
|
|
23
28
|
def self.normalize_namespace: (Symbol | String | nil namespace) -> Symbol
|
|
24
29
|
def self.register: (String file_path, ?namespace: Symbol | String | nil, **Hash[untyped, untyped] options) -> void
|
|
30
|
+
def self.register_eager: (String file_path, ?namespace: Symbol | String | nil, **Hash[untyped, untyped] options) -> void
|
|
25
31
|
def self.register_inline: (String css_content, ?namespace: Symbol | String | nil, ?capsule_id: String | nil, ?cache_key: String | nil, ?cache_strategy: Symbol, ?cache_ttl: Integer | ActiveSupport::Duration | nil, ?cache_proc: Proc | nil, ?component_class: Class | nil, ?stylesheet_link_options: Hash[untyped, untyped] | nil) -> void
|
|
26
32
|
def self.cached_inline: (String cache_key, cache_strategy: Symbol, ?cache_ttl: Integer | ActiveSupport::Duration | nil, ?cache_proc: Proc | nil, ?css_content: String | nil, ?capsule_id: String | nil, ?namespace: Symbol | nil) -> String | nil
|
|
27
33
|
def self.cache_inline_css: (String cache_key, String css_content, cache_strategy: Symbol, ?cache_ttl: Integer | ActiveSupport::Duration | nil, ?cache_proc: Proc | nil, ?capsule_id: String | nil, ?namespace: Symbol | nil) -> void
|
|
28
34
|
def self.clear_inline_cache: (?String cache_key) -> void
|
|
29
35
|
def self.cleanup_expired_cache: () -> Integer
|
|
30
|
-
def self.manifest_files: () -> Hash[Symbol,
|
|
36
|
+
def self.manifest_files: () -> Hash[Symbol, Array[Hash[Symbol, untyped]]]
|
|
31
37
|
def self.request_inline_stylesheets: () -> Hash[Symbol, Array[Hash[Symbol, untyped]]]
|
|
38
|
+
def self.request_stylesheet_files: () -> Hash[Symbol, Hash[String, Hash[Symbol, untyped]]]
|
|
32
39
|
def self.stylesheets_for: (?namespace: Symbol | String | nil) -> Array[Hash[Symbol, untyped]]
|
|
33
40
|
def self.clear: (?namespace: Symbol | String | nil) -> void
|
|
34
41
|
def self.clear_manifest: (?namespace: Symbol | String | nil) -> void
|
|
35
42
|
def self.render_head_stylesheets: (?ActionView::Base | nil view_context, ?namespace: Symbol | String | nil) -> String
|
|
43
|
+
def self.inject_pending_head_stylesheets: (String html, ?ActionView::Base | nil view_context) -> String
|
|
44
|
+
def self.pending_head_stylesheets?: () -> bool
|
|
36
45
|
def self.any?: (?namespace: Symbol | String | nil) -> bool
|
|
37
46
|
end
|
|
38
47
|
|
|
48
|
+
class HeadInjectionMiddleware
|
|
49
|
+
def initialize: (untyped app) -> void
|
|
50
|
+
def call: (Hash[Symbol, untyped] env) -> [Integer, Hash[String, String], untyped]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
module ComponentClassMethods
|
|
54
|
+
MAX_CSS_CACHE_ENTRIES: Integer
|
|
55
|
+
def css_cache: () -> Hash[String, String]
|
|
56
|
+
def store_css_cache: (String cache_key, String scoped_css) -> String
|
|
57
|
+
def clear_css_cache: () -> void
|
|
58
|
+
def capsule_id: () -> String | nil
|
|
59
|
+
def capsule_id: (String capsule_id) -> String
|
|
60
|
+
def stylesheet_registry: (?namespace: Symbol | String | nil, ?cache_strategy: Symbol, ?cache_ttl: Integer | ActiveSupport::Duration | nil, ?cache_proc: Proc | nil) -> void
|
|
61
|
+
def style_capsule: (?namespace: Symbol | String | nil, ?cache_strategy: Symbol | String | Proc | nil, ?cache_ttl: Integer | ActiveSupport::Duration | nil, ?cache_proc: Proc | nil, ?scoping_strategy: Symbol | nil, ?head_rendering: bool | nil, ?tag: Symbol | String | nil) -> void
|
|
62
|
+
def head_rendering?: () -> bool
|
|
63
|
+
def stylesheet_namespace: () -> Symbol | String | nil
|
|
64
|
+
def custom_capsule_id: () -> String | nil
|
|
65
|
+
def inline_cache_strategy: () -> Symbol | nil
|
|
66
|
+
def inline_cache_strategy: (Symbol strategy, ?ttl: Integer | nil, ?cache_proc: Proc | nil) -> Symbol
|
|
67
|
+
def inline_cache_ttl: () -> Integer | ActiveSupport::Duration | nil
|
|
68
|
+
def inline_cache_proc: () -> Proc | nil
|
|
69
|
+
def stylesheet_link_options: () -> Hash[untyped, untyped] | nil
|
|
70
|
+
def stylesheet_link_options: (Hash[untyped, untyped] options) -> Hash[untyped, untyped]
|
|
71
|
+
def css_scoping_strategy: () -> Symbol
|
|
72
|
+
end
|
|
73
|
+
|
|
39
74
|
module Component
|
|
40
75
|
module ClassMethods
|
|
41
|
-
|
|
42
|
-
def clear_css_cache: () -> void
|
|
43
|
-
def capsule_id: () -> String | nil
|
|
44
|
-
def capsule_id: (String capsule_id) -> String
|
|
45
|
-
def stylesheet_registry: (?namespace: Symbol | String | nil, ?cache_strategy: Symbol, ?cache_ttl: Integer | ActiveSupport::Duration | nil, ?cache_proc: Proc | nil) -> void
|
|
46
|
-
def head_rendering!: () -> void # deprecated
|
|
47
|
-
def head_rendering?: () -> bool
|
|
48
|
-
def stylesheet_namespace: () -> Symbol | String | nil
|
|
49
|
-
def custom_capsule_id: () -> String | nil
|
|
50
|
-
def inline_cache_strategy: () -> Symbol | nil
|
|
51
|
-
def inline_cache_strategy: (Symbol strategy, ?ttl: Integer | nil, ?cache_proc: Proc | nil) -> Symbol
|
|
52
|
-
def inline_cache_ttl: () -> Integer | ActiveSupport::Duration | nil
|
|
53
|
-
def inline_cache_proc: () -> Proc | nil
|
|
54
|
-
def stylesheet_link_options: () -> Hash[untyped, untyped] | nil
|
|
55
|
-
def stylesheet_link_options: (Hash[untyped, untyped] options) -> Hash[untyped, untyped]
|
|
56
|
-
def css_scoping_strategy: () -> Symbol
|
|
57
|
-
def css_scoping_strategy: (Symbol strategy) -> Symbol
|
|
76
|
+
include ComponentClassMethods
|
|
58
77
|
end
|
|
59
78
|
|
|
60
79
|
def component_capsule: () -> String
|
|
@@ -63,23 +82,7 @@ module StyleCapsule
|
|
|
63
82
|
|
|
64
83
|
module ViewComponent
|
|
65
84
|
module ClassMethods
|
|
66
|
-
|
|
67
|
-
def clear_css_cache: () -> void
|
|
68
|
-
def capsule_id: () -> String | nil
|
|
69
|
-
def capsule_id: (String capsule_id) -> String
|
|
70
|
-
def stylesheet_registry: (?namespace: Symbol | String | nil, ?cache_strategy: Symbol, ?cache_ttl: Integer | ActiveSupport::Duration | nil, ?cache_proc: Proc | nil) -> void
|
|
71
|
-
def head_rendering!: () -> void # deprecated
|
|
72
|
-
def head_rendering?: () -> bool
|
|
73
|
-
def stylesheet_namespace: () -> Symbol | String | nil
|
|
74
|
-
def custom_capsule_id: () -> String | nil
|
|
75
|
-
def inline_cache_strategy: () -> Symbol | nil
|
|
76
|
-
def inline_cache_strategy: (Symbol strategy, ?ttl: Integer | nil, ?cache_proc: Proc | nil) -> Symbol
|
|
77
|
-
def inline_cache_ttl: () -> Integer | ActiveSupport::Duration | nil
|
|
78
|
-
def inline_cache_proc: () -> Proc | nil
|
|
79
|
-
def stylesheet_link_options: () -> Hash[untyped, untyped] | nil
|
|
80
|
-
def stylesheet_link_options: (Hash[untyped, untyped] options) -> Hash[untyped, untyped]
|
|
81
|
-
def css_scoping_strategy: () -> Symbol
|
|
82
|
-
def css_scoping_strategy: (Symbol strategy) -> Symbol
|
|
85
|
+
include ComponentClassMethods
|
|
83
86
|
end
|
|
84
87
|
|
|
85
88
|
def component_capsule: () -> String
|
|
@@ -89,25 +92,21 @@ module StyleCapsule
|
|
|
89
92
|
module ViewComponentHelper
|
|
90
93
|
def register_stylesheet: (String file_path, ?namespace: Symbol | String | nil, **Hash[untyped, untyped] options) -> void
|
|
91
94
|
def stylesheet_registry_tags: (?namespace: Symbol | String | nil) -> String
|
|
92
|
-
def stylesheet_registrymap_tags: (?namespace: Symbol | String | nil) -> String # deprecated
|
|
93
95
|
end
|
|
94
96
|
|
|
95
97
|
module Helper
|
|
96
98
|
def generate_capsule_id: (String css_content) -> String
|
|
97
99
|
def scope_css: (String css_content, String capsule_id) -> String
|
|
98
|
-
def style_capsule: (?String css_content, ?capsule_id: String | nil) { () -> String } -> String
|
|
100
|
+
def style_capsule: (?String css_content, ?capsule_id: String | nil, ?tag: Symbol | String) { () -> String } -> String
|
|
99
101
|
def register_stylesheet: (String file_path, ?namespace: Symbol | String | nil, **Hash[untyped, untyped] options) -> void
|
|
100
102
|
def stylesheet_registry_tags: (?namespace: Symbol | String | nil) -> String
|
|
101
|
-
def stylesheet_registrymap_tags: (?namespace: Symbol | String | nil) -> String # deprecated
|
|
102
103
|
end
|
|
103
104
|
|
|
104
105
|
module PhlexHelper
|
|
105
106
|
def register_stylesheet: (String file_path, ?namespace: Symbol | String | nil, **Hash[untyped, untyped] options) -> void
|
|
106
|
-
def stylesheet_registry_tags: (?namespace: Symbol | String | nil) ->
|
|
107
|
-
def stylesheet_registrymap_tags: (?namespace: Symbol | String | nil) -> void # deprecated
|
|
107
|
+
def stylesheet_registry_tags: (?namespace: Symbol | String | nil) -> String
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
class Railtie < Rails::Railtie
|
|
111
111
|
end
|
|
112
112
|
end
|
|
113
|
-
|