style_capsule 1.2.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.
@@ -0,0 +1,482 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha1"
4
+ # ActiveSupport string extensions are conditionally required in lib/style_capsule.rb
5
+
6
+ module StyleCapsule
7
+ # ViewComponent component concern for encapsulated CSS
8
+ #
9
+ # This implements attribute-based CSS scoping for component encapsulation:
10
+ # - CSS selectors are rewritten to include [data-capsule="..."] attribute selectors
11
+ # - Class names remain unchanged (no renaming)
12
+ # - Scope ID is per-component-type (shared across all instances)
13
+ # - Styles are rendered as <style> tag in body before component HTML
14
+ # - Automatically wraps component content in a scoped wrapper element
15
+ #
16
+ # Usage in a ViewComponent component:
17
+ #
18
+ # Instance method (dynamic rendering, all cache strategies except :file):
19
+ #
20
+ # class MyComponent < ApplicationComponent
21
+ # include StyleCapsule::ViewComponent
22
+ #
23
+ # def component_styles
24
+ # <<~CSS
25
+ # .section {
26
+ # color: red;
27
+ # }
28
+ # .heading:hover {
29
+ # opacity: 0.8;
30
+ # }
31
+ # CSS
32
+ # end
33
+ # end
34
+ #
35
+ # Class method (static rendering, supports all cache strategies including :file):
36
+ #
37
+ # class MyComponent < ApplicationComponent
38
+ # include StyleCapsule::ViewComponent
39
+ # stylesheet_registry cache_strategy: :file # File caching requires class method
40
+ #
41
+ # def self.component_styles
42
+ # <<~CSS
43
+ # .section {
44
+ # color: red;
45
+ # }
46
+ # CSS
47
+ # end
48
+ #
49
+ # def call
50
+ # # Content is automatically wrapped in a scoped element
51
+ # # No need to manually add data-capsule attribute!
52
+ # content_tag(:div, class: "section") do
53
+ # content_tag(:h2, "Hello", class: "heading")
54
+ # end
55
+ # end
56
+ # end
57
+ #
58
+ # For testing with a custom scope ID:
59
+ #
60
+ # class MyComponent < ApplicationComponent
61
+ # include StyleCapsule::ViewComponent
62
+ # capsule_id "test-capsule-123" # Use exact capsule ID for testing
63
+ #
64
+ # def component_styles
65
+ # <<~CSS
66
+ # .section { color: red; }
67
+ # CSS
68
+ # end
69
+ # end
70
+ #
71
+ # The CSS will be automatically rewritten from:
72
+ # .section { color: red; }
73
+ # .heading:hover { opacity: 0.8; }
74
+ #
75
+ # To:
76
+ # [data-capsule="a1b2c3d4"] .section { color: red; }
77
+ # [data-capsule="a1b2c3d4"] .heading:hover { opacity: 0.8; }
78
+ #
79
+ # And the HTML will be automatically wrapped:
80
+ # <div data-capsule="a1b2c3d4">
81
+ # <div class="section">...</div>
82
+ # </div>
83
+ #
84
+ # This ensures styles only apply to elements within the scoped component.
85
+ module ViewComponent
86
+ def self.included(base)
87
+ base.extend(ClassMethods)
88
+ base.include(ComponentStylesSupport)
89
+
90
+ # Use prepend to wrap call method
91
+ base.prepend(CallWrapper)
92
+
93
+ # Register class for Rails-friendly tracking
94
+ ClassRegistry.register(base)
95
+ end
96
+
97
+ 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::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
344
+ end
345
+
346
+ # Module that wraps call to add scoped wrapper
347
+ module CallWrapper
348
+ def call
349
+ if component_styles?
350
+ # Render styles first
351
+ styles_html = render_capsule_styles
352
+
353
+ # Get content from original call method
354
+ content_html = super
355
+
356
+ # Wrap content in scoped element
357
+ scoped_wrapper = helpers.content_tag(:div, content_html.html_safe, data: {capsule: component_capsule})
358
+
359
+ # Combine styles and wrapped content
360
+ (styles_html + scoped_wrapper).html_safe
361
+ else
362
+ # No styles, render normally
363
+ super
364
+ end
365
+ end
366
+ end
367
+
368
+ # Get the component capsule ID (per-component-type, shared across instances)
369
+ #
370
+ # All instances of the same component class share the same capsule ID.
371
+ # Can be overridden with capsule_id class method for testing.
372
+ #
373
+ # @return [String] The capsule ID (e.g., "a1b2c3d4")
374
+ def component_capsule
375
+ return @component_capsule if defined?(@component_capsule)
376
+
377
+ # Check for custom capsule ID set via class method
378
+ @component_capsule = self.class.custom_capsule_id || generate_capsule_id
379
+ end
380
+
381
+ private
382
+
383
+ # Render the style capsule <style> tag
384
+ #
385
+ # Can render in body (default) or register for head rendering via StylesheetRegistry
386
+ #
387
+ # Supports both instance method (def component_styles) and class method (def self.component_styles).
388
+ # File caching is only allowed for class method component_styles.
389
+ #
390
+ # @return [String] HTML string with style tag or empty string
391
+ def render_capsule_styles
392
+ css_content = component_styles_content
393
+ return "".html_safe if css_content.nil? || css_content.to_s.strip.empty?
394
+
395
+ scoped_css = scope_css(css_content)
396
+ capsule_id = component_capsule
397
+
398
+ # Check if component uses head rendering
399
+ if head_rendering?
400
+ # Register for head rendering instead of rendering in body
401
+ namespace = self.class.stylesheet_namespace
402
+
403
+ # Get cache configuration from class
404
+ cache_strategy = self.class.inline_cache_strategy || :none
405
+ cache_ttl = self.class.inline_cache_ttl
406
+ cache_proc = self.class.inline_cache_proc
407
+
408
+ # File caching is only allowed for class method component_styles
409
+ if cache_strategy == :file && !file_caching_allowed?
410
+ # Fall back to :none strategy if file caching requested but not allowed
411
+ cache_strategy = :none
412
+ cache_ttl = nil
413
+ cache_proc = nil
414
+ end
415
+
416
+ # Generate cache key based on component class and capsule
417
+ cache_key = (cache_strategy != :none) ? "#{self.class.name}:#{capsule_id}" : nil
418
+
419
+ StylesheetRegistry.register_inline(
420
+ scoped_css,
421
+ namespace: namespace,
422
+ capsule_id: capsule_id,
423
+ cache_key: cache_key,
424
+ cache_strategy: cache_strategy,
425
+ cache_ttl: cache_ttl,
426
+ cache_proc: cache_proc,
427
+ component_class: self.class,
428
+ stylesheet_link_options: self.class.stylesheet_link_options
429
+ )
430
+ "".html_safe
431
+ else
432
+ # Render <style> tag in body (HTML5 allows this)
433
+ helpers.content_tag(:style, scoped_css.html_safe, type: "text/css")
434
+ end
435
+ end
436
+
437
+ # Check if component should use head rendering
438
+ #
439
+ # Checks class-level configuration first, then allows instance override.
440
+ def head_rendering?
441
+ return true if self.class.head_rendering?
442
+ false
443
+ end
444
+
445
+ # Scope CSS and return scoped CSS with attribute selectors
446
+ 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
+ capsule_id = component_capsule
450
+ scoping_strategy = self.class.css_scoping_strategy
451
+ cache_key = "#{self.class.name}:#{capsule_id}:#{scoping_strategy}"
452
+
453
+ if self.class.css_cache.key?(cache_key)
454
+ return self.class.css_cache[cache_key]
455
+ end
456
+
457
+ # Use the configured scoping strategy
458
+ scoped_css = case scoping_strategy
459
+ when :nesting
460
+ CssProcessor.scope_with_nesting(css_content, capsule_id)
461
+ else # :selector_patching (default)
462
+ CssProcessor.scope_selectors(css_content, capsule_id)
463
+ end
464
+
465
+ # Cache at class level (one style block per component type/scope/strategy combination)
466
+ self.class.css_cache[cache_key] = scoped_css
467
+
468
+ scoped_css
469
+ end
470
+
471
+ # Generate a unique capsule ID based on component class name (per-component-type)
472
+ #
473
+ # This ensures all instances of the same component class share the same capsule ID,
474
+ # similar to how component-based frameworks scope styles per component type.
475
+ #
476
+ # @return [String] The capsule ID (e.g., "a1b2c3d4")
477
+ def generate_capsule_id
478
+ class_name = self.class.name || self.class.object_id.to_s
479
+ "a#{Digest::SHA1.hexdigest(class_name)}"[0, 8]
480
+ end
481
+ end
482
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StyleCapsule
4
+ # ViewComponent helper module for StyleCapsule stylesheet registry
5
+ #
6
+ # Include this in your base ViewComponent class (e.g., ApplicationComponent):
7
+ # class ApplicationComponent < ViewComponent::Base
8
+ # include StyleCapsule::ViewComponentHelper
9
+ # end
10
+ #
11
+ # Usage in ViewComponent layouts:
12
+ # def call
13
+ # helpers.stylesheet_registrymap_tags
14
+ # end
15
+ #
16
+ # Usage in ViewComponent components:
17
+ # def call
18
+ # register_stylesheet("stylesheets/user/my_component")
19
+ # content_tag(:div, "Content")
20
+ # end
21
+ module ViewComponentHelper
22
+ # Register a stylesheet file for head rendering
23
+ #
24
+ # Usage in ViewComponent components:
25
+ # def call
26
+ # register_stylesheet("stylesheets/user/my_component", "data-turbo-track": "reload")
27
+ # register_stylesheet("stylesheets/admin/dashboard", namespace: :admin)
28
+ # content_tag(:div, "Content")
29
+ # end
30
+ #
31
+ # @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)
33
+ # @param options [Hash] Options for stylesheet_link_tag
34
+ # @return [void]
35
+ def register_stylesheet(file_path, namespace: nil, **options)
36
+ StyleCapsule::StylesheetRegistry.register(file_path, namespace: namespace, **options)
37
+ end
38
+
39
+ # Render StyleCapsule registered stylesheets
40
+ #
41
+ # Usage in ViewComponent layouts:
42
+ # def call
43
+ # helpers.stylesheet_registry_tags
44
+ # helpers.stylesheet_registry_tags(namespace: :admin)
45
+ # end
46
+ #
47
+ # @param namespace [Symbol, String, nil] Optional namespace to render (nil/blank renders all)
48
+ # @return [String] HTML-safe string with stylesheet tags
49
+ def stylesheet_registry_tags(namespace: nil)
50
+ StyleCapsule::StylesheetRegistry.render_head_stylesheets(helpers, namespace: namespace)
51
+ 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
+ end
57
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha1"
4
+ # Conditionally require ActiveSupport string extensions if available
5
+ # For non-Rails usage, these are optional
6
+ # Check first to avoid exception handling overhead in common case (Rails apps)
7
+ unless defined?(ActiveSupport) || String.method_defined?(:html_safe)
8
+ begin
9
+ require "active_support/core_ext/string"
10
+ rescue LoadError
11
+ # ActiveSupport not available - core functionality still works
12
+ end
13
+ end
14
+
15
+ # StyleCapsule provides attribute-based CSS scoping for component encapsulation
16
+ # in Phlex components, ViewComponent components, and ERB templates.
17
+ #
18
+ # @example Phlex Component Usage
19
+ # class MyComponent < ApplicationComponent
20
+ # include StyleCapsule::Component
21
+ #
22
+ # def component_styles
23
+ # <<~CSS
24
+ # .section { color: red; }
25
+ # CSS
26
+ # end
27
+ # end
28
+ #
29
+ # @example ViewComponent Encapsulation Usage
30
+ # class MyComponent < ApplicationComponent
31
+ # include StyleCapsule::ViewComponent
32
+ #
33
+ # def component_styles
34
+ # <<~CSS
35
+ # .section { color: red; }
36
+ # .heading:hover { opacity: 0.8; }
37
+ # CSS
38
+ # end
39
+ #
40
+ # def call
41
+ # # Content is automatically wrapped in a scoped element
42
+ # content_tag(:div, class: "section") do
43
+ # content_tag(:h2, "Hello", class: "heading")
44
+ # end
45
+ # end
46
+ # end
47
+ #
48
+ # @example ViewComponent Helper Usage (for stylesheet registry)
49
+ # class ApplicationComponent < ViewComponent::Base
50
+ # include StyleCapsule::ViewComponentHelper
51
+ # end
52
+ #
53
+ # class MyComponent < ApplicationComponent
54
+ # def call
55
+ # register_stylesheet("stylesheets/user/my_component")
56
+ # content_tag(:div, "Content", class: "section")
57
+ # end
58
+ # end
59
+ #
60
+ # @example ERB Helper Usage
61
+ # # Helpers are automatically included - no setup required
62
+ # <%= style_capsule do %>
63
+ # <style>.section { color: red; }</style>
64
+ # <div class="section">Content</div>
65
+ # <% end %>
66
+ #
67
+ # <%= stylesheet_registry_tags %>
68
+ # <%= stylesheet_registry_tags(namespace: :admin) %>
69
+ #
70
+ # @example Namespace Support
71
+ # # Register stylesheets with namespaces
72
+ # StyleCapsule::StylesheetRegistry.register('stylesheets/admin/dashboard', namespace: :admin)
73
+ # StyleCapsule::StylesheetRegistry.register('stylesheets/user/profile', namespace: :user)
74
+ #
75
+ # # Render all namespaces (default)
76
+ # <%= stylesheet_registry_tags %>
77
+ #
78
+ # # Render specific namespace
79
+ # <%= stylesheet_registry_tags(namespace: :admin) %>
80
+ #
81
+ # @example File-Based Caching (HTTP Caching)
82
+ # class MyComponent < ApplicationComponent
83
+ # include StyleCapsule::Component
84
+ # stylesheet_registry cache_strategy: :file # Writes CSS to files for HTTP caching
85
+ # end
86
+ #
87
+ # # CSS files are written to app/assets/builds/capsules/
88
+ # # Files are automatically precompiled via: bin/rails assets:precompile
89
+ # # Or manually: bin/rails style_capsule:build
90
+ module StyleCapsule
91
+ require_relative "style_capsule/version"
92
+ require_relative "style_capsule/css_processor"
93
+ require_relative "style_capsule/css_file_writer"
94
+ require_relative "style_capsule/stylesheet_registry"
95
+ require_relative "style_capsule/class_registry"
96
+ require_relative "style_capsule/component_styles_support"
97
+ require_relative "style_capsule/component"
98
+ require_relative "style_capsule/standalone_helper"
99
+ require_relative "style_capsule/helper"
100
+ require_relative "style_capsule/phlex_helper"
101
+ require_relative "style_capsule/view_component"
102
+ require_relative "style_capsule/view_component_helper"
103
+ require_relative "style_capsule/component_builder"
104
+ require_relative "style_capsule/railtie" if defined?(Rails) && defined?(Rails::Railtie)
105
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :style_capsule do
4
+ desc "Build StyleCapsule CSS files from components (similar to Tailwind CSS build)"
5
+ task build: :environment do
6
+ require "style_capsule/component_builder"
7
+
8
+ StyleCapsule::ComponentBuilder.build_all(output_proc: ->(msg) { puts msg })
9
+ end
10
+
11
+ desc "Clear StyleCapsule generated CSS files"
12
+ task clear: :environment do
13
+ require "style_capsule/css_file_writer"
14
+ StyleCapsule::CssFileWriter.clear_files
15
+ puts "StyleCapsule CSS files cleared"
16
+ end
17
+ end
18
+
19
+ # Hook into Rails asset precompilation (similar to Tailwind CSS)
20
+ if defined?(Rails)
21
+ Rake::Task["assets:precompile"].enhance(["style_capsule:build"]) if Rake::Task.task_defined?("assets:precompile")
22
+ end