style_capsule 1.2.0 → 1.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1fcbe6c03f9675593d6dca7b3d7d70de2b7d471d2d938993b59877614332ff4
4
- data.tar.gz: 6aa5feb7ff25ef755084e7754f328e4340a0d4dc620e1ea8298badbb0387f254
3
+ metadata.gz: 74203df88bc23dc0b1509f0e7adc5978fe327c982026916e990644c92f65a0db
4
+ data.tar.gz: 7f7783ef50b66e14859e1f7207262279f20e5f28eb29d3373a85fef9ae2f3a7c
5
5
  SHA512:
6
- metadata.gz: f06af1e47282aa0220b5ff28e5e0a197a8d76eaa9f3c00639958ca1f9d672c7ad1dd4f672b9de83cc7c521af6a49b9c9f9c9bd0ce476575211b1fc7a7aeb1da2
7
- data.tar.gz: 3844dd9da6608b1eff6e495e19b92e3f90d4b41df323118f5fa7c6216603d3b33620c80df8aeb13c2e2506e133e3ab6e424c3dfd76feb16dbbc68b37fac72636
6
+ metadata.gz: bed787978d271158ca409ffe8445df1f6203e954000d153beae40368861e0077b566557c3f50456373d04998f85ab60ee86b3cd93ed558d31f3d482247fb0b7e
7
+ data.tar.gz: 22b900ebbf5ce4a219d487f7915b7dcd31ed4552d134af44f056ba79eff8551a4ef62e8070f03887fc4962f2073d06bd3b2d65d35e00f09183d97a730ad24ee2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.4.0 (2025-11-26)
4
+
5
+ - Added unified `style_capsule` class method for configuring all StyleCapsule settings (namespace, cache strategy, CSS scoping, head rendering) in a single call
6
+ - Added automatic namespace fallback in `register_stylesheet` helper methods - when namespace is not specified, uses the component's configured namespace from `style_capsule`
7
+ - Removed deprecated `head_rendering!` method (use `style_capsule` instead)
8
+ - Removed deprecated `stylesheet_registrymap_tags` alias (use `stylesheet_registry_tags` instead)
9
+ - Refactored namespace configuration to use instance variables instead of constants for better inheritance behavior
10
+
11
+ ## 1.3.0 (2025-11-26)
12
+
13
+ - Added comprehensive instrumentation via `StyleCapsule::Instrumentation` using ActiveSupport::Notifications
14
+ - Instrumentation events for CSS processing (`style_capsule.css_processor.scope`) with duration and size metrics
15
+ - Instrumentation events for CSS file writing (`style_capsule.css_file_writer.write`) with duration and size metrics
16
+ - Added fallback directory support for CSS file writing when default location is read-only (e.g., Docker containers)
17
+ - Automatic fallback to `/tmp/style_capsule` when primary output directory is not writable
18
+ - Instrumentation events for fallback scenarios (`style_capsule.css_file_writer.fallback`, `style_capsule.css_file_writer.fallback_failure`)
19
+ - Instrumentation events for write failures (`style_capsule.css_file_writer.write_failure`)
20
+ - All instrumentation is zero-overhead when no subscribers are present (only calculates metrics when actively monitored)
21
+ - Improved test coverage reporting and analysis tools
22
+ - Added community guidelines and governance documents
23
+
3
24
  ## 1.2.0 (2025-11-24)
4
25
 
5
26
  - Added `StyleCapsule::ClassRegistry` for Rails-friendly class tracking without ObjectSpace iteration
@@ -16,7 +37,7 @@
16
37
  - Rails integration remains fully supported via Railtie
17
38
  - Added `StyleCapsule::StandaloneHelper` for non-Rails frameworks
18
39
  - `StylesheetRegistry` now works without `ActiveSupport::CurrentAttributes` using thread-local storage fallback
19
- - Renamed `stylesheet_registrymap_tags` to `stylesheet_registry_tags` (old name kept as deprecated alias)
40
+ - Renamed `stylesheet_registrymap_tags` to `stylesheet_registry_tags` (deprecated alias removed in 1.4.0)
20
41
  - Extracted CSS building logic from Rake tasks into `StyleCapsule::ComponentBuilder`
21
42
  - Fixed XSS vulnerability in `escape_html_attr` by using `CGI.escapeHTML` for proper HTML entity escaping
22
43
  - Optimized ActiveSupport require to avoid exception handling overhead in Rails apps
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # style_capsule
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/style_capsule.svg?v=1.2.0)](https://badge.fury.io/rb/style_capsule) [![Test Status](https://github.com/amkisko/style_capsule.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/style_capsule.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/style_capsule.rb/graph/badge.svg?token=2U6NXJOVVM)](https://codecov.io/gh/amkisko/style_capsule.rb)
3
+ [![Gem Version](https://badge.fury.io/rb/style_capsule.svg?v=1.4.0)](https://badge.fury.io/rb/style_capsule) [![Test Status](https://github.com/amkisko/style_capsule.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/style_capsule.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/style_capsule.rb/graph/badge.svg?token=2U6NXJOVVM)](https://codecov.io/gh/amkisko/style_capsule.rb)
4
4
 
5
5
  CSS scoping extension for Ruby components. Provides attribute-based style encapsulation for Phlex, ViewComponent, and ERB templates to prevent style leakage between components. Works with Rails and can be used standalone in other Ruby frameworks (Sinatra, Hanami, etc.) or plain Ruby scripts. Includes configurable caching strategies for optimal performance.
6
6
 
@@ -28,6 +28,8 @@ Then run `bundle install`.
28
28
  - **CSS Nesting support** (optional, more performant, requires modern browsers)
29
29
  - **Stylesheet registry** with thread-safe head rendering, namespace support, and compatibility with Propshaft and other asset bundlers
30
30
  - **Multiple cache strategies**: none, time-based, custom proc, and file-based (HTTP caching)
31
+ - **Comprehensive instrumentation** via ActiveSupport::Notifications for monitoring and metrics
32
+ - **Fallback directory support** for read-only filesystems (e.g., Docker containers)
31
33
  - **Security protections**: path traversal protection, input validation, size limits
32
34
 
33
35
  ## Usage
@@ -101,12 +103,12 @@ StyleCapsule supports two CSS scoping strategies:
101
103
 
102
104
  ### Configuration
103
105
 
104
- **Per-component:**
106
+ **Per-component (using `style_capsule` - recommended):**
105
107
 
106
108
  ```ruby
107
109
  class MyComponent < ApplicationComponent
108
110
  include StyleCapsule::Component
109
- css_scoping_strategy :nesting # Use CSS nesting
111
+ style_capsule scoping_strategy: :nesting # Use CSS nesting
110
112
  end
111
113
  ```
112
114
 
@@ -115,7 +117,7 @@ end
115
117
  ```ruby
116
118
  class ApplicationComponent < Phlex::HTML
117
119
  include StyleCapsule::Component
118
- css_scoping_strategy :nesting # Enable for all components
120
+ style_capsule scoping_strategy: :nesting # Enable for all components
119
121
  end
120
122
  ```
121
123
 
@@ -127,12 +129,12 @@ MyComponent.clear_css_cache
127
129
 
128
130
  ## Stylesheet Registry
129
131
 
130
- For better performance, register styles for head rendering instead of rendering `<style>` tags in the body:
132
+ For better performance, register styles for head rendering instead of rendering `<style>` tags in the body. Use the unified `style_capsule` method to configure all settings:
131
133
 
132
134
  ```ruby
133
135
  class MyComponent < ApplicationComponent
134
136
  include StyleCapsule::Component
135
- stylesheet_registry namespace: :admin # Optional namespace
137
+ style_capsule namespace: :admin # Configure namespace and enable head rendering
136
138
 
137
139
  def component_styles
138
140
  <<~CSS
@@ -142,12 +144,17 @@ class MyComponent < ApplicationComponent
142
144
  end
143
145
  ```
144
146
 
145
- With cache strategy:
147
+ With cache strategy and CSS scoping:
146
148
 
147
149
  ```ruby
148
150
  class MyComponent < ApplicationComponent
149
151
  include StyleCapsule::Component
150
- stylesheet_registry namespace: :admin, cache_strategy: :time, cache_ttl: 1.hour
152
+ style_capsule(
153
+ namespace: :admin,
154
+ cache_strategy: :time,
155
+ cache_ttl: 1.hour,
156
+ scoping_strategy: :nesting
157
+ )
151
158
 
152
159
  def component_styles
153
160
  <<~CSS
@@ -157,11 +164,10 @@ class MyComponent < ApplicationComponent
157
164
  end
158
165
  ```
159
166
 
160
- Then in your layout:
167
+ Then in your layout (render only the namespace you need):
161
168
 
162
169
  ```erb
163
170
  <head>
164
- <%= stylesheet_registry_tags %>
165
171
  <%= stylesheet_registry_tags(namespace: :admin) %>
166
172
  </head>
167
173
  ```
@@ -170,42 +176,58 @@ Or in Phlex (requires including `StyleCapsule::PhlexHelper`):
170
176
 
171
177
  ```ruby
172
178
  head do
173
- stylesheet_registry_tags
179
+ stylesheet_registry_tags(namespace: :admin)
174
180
  end
175
181
  ```
176
182
 
183
+ **Namespace Isolation:** Using namespaces prevents stylesheet leakage between different application contexts. For example, login pages can use `namespace: :login`, ActiveAdmin can use `namespace: :active_admin`, and user components can use `namespace: :user`. Each namespace is rendered separately, improving caching efficiency and preventing style conflicts.
184
+
177
185
  ### Registering Stylesheet Files
178
186
 
179
- You can also register external stylesheet files (not inline CSS) for head rendering:
187
+ You can also register external stylesheet files (not inline CSS) for head rendering. When a component has a configured namespace via `style_capsule`, you don't need to specify it every time:
180
188
 
181
189
  **In ERB:**
182
190
 
183
191
  ```erb
184
- <% register_stylesheet("stylesheets/user/order_select_component", "data-turbo-track": "reload") %>
192
+ <% register_stylesheet("stylesheets/user/my_component", "data-turbo-track": "reload") %>
185
193
  <% register_stylesheet("stylesheets/admin/dashboard", namespace: :admin) %>
186
194
  ```
187
195
 
188
196
  **In Phlex (requires including `StyleCapsule::PhlexHelper`):**
189
197
 
190
198
  ```ruby
191
- def view_template
192
- register_stylesheet("stylesheets/user/order_select_component", "data-turbo-track": "reload")
193
- register_stylesheet("stylesheets/admin/dashboard", namespace: :admin)
194
- div { "Content" }
199
+ class UserComponent < ApplicationComponent
200
+ include StyleCapsule::Component
201
+ include StyleCapsule::PhlexHelper
202
+ style_capsule namespace: :user # Set default namespace
203
+
204
+ def view_template
205
+ # Namespace automatically uses :user from style_capsule
206
+ register_stylesheet("stylesheets/user/my_component", "data-turbo-track": "reload")
207
+ # Can still override namespace if needed
208
+ register_stylesheet("stylesheets/shared/common", namespace: :shared)
209
+ div { "Content" }
210
+ end
195
211
  end
196
212
  ```
197
213
 
198
214
  **In ViewComponent (requires including `StyleCapsule::ViewComponentHelper`):**
199
215
 
200
216
  ```ruby
201
- def call
202
- register_stylesheet("stylesheets/user/order_select_component", "data-turbo-track": "reload")
203
- register_stylesheet("stylesheets/admin/dashboard", namespace: :admin)
204
- content_tag(:div, "Content")
217
+ class UserComponent < ApplicationComponent
218
+ include StyleCapsule::ViewComponent
219
+ include StyleCapsule::ViewComponentHelper
220
+ style_capsule namespace: :user # Set default namespace
221
+
222
+ def call
223
+ # Namespace automatically uses :user from style_capsule
224
+ register_stylesheet("stylesheets/user/my_component", "data-turbo-track": "reload")
225
+ content_tag(:div, "Content")
226
+ end
205
227
  end
206
228
  ```
207
229
 
208
- Registered files are rendered via `stylesheet_registry_tags` in your layout, just like inline CSS.
230
+ Registered files are rendered via `stylesheet_registry_tags` in your layout, just like inline CSS. The namespace is automatically used from the component's `style_capsule` configuration when not explicitly specified.
209
231
 
210
232
  ## Caching Strategies
211
233
 
@@ -214,22 +236,22 @@ Registered files are rendered via `stylesheet_registry_tags` in your layout, jus
214
236
  ```ruby
215
237
  class MyComponent < ApplicationComponent
216
238
  include StyleCapsule::Component
217
- stylesheet_registry # No cache strategy set (default: :none)
239
+ style_capsule # No cache strategy set (default: :none)
218
240
  end
219
241
  ```
220
242
 
221
243
  ### Time-Based Caching
222
244
 
223
245
  ```ruby
224
- stylesheet_registry cache_strategy: :time, cache_ttl: 1.hour # Using ActiveSupport::Duration
246
+ style_capsule cache_strategy: :time, cache_ttl: 1.hour # Using ActiveSupport::Duration
225
247
  # Or using integer seconds:
226
- stylesheet_registry cache_strategy: :time, cache_ttl: 3600 # Cache for 1 hour
248
+ style_capsule cache_strategy: :time, cache_ttl: 3600 # Cache for 1 hour
227
249
  ```
228
250
 
229
251
  ### Custom Proc Caching
230
252
 
231
253
  ```ruby
232
- stylesheet_registry cache_strategy: ->(css, capsule_id, namespace) {
254
+ style_capsule cache_strategy: ->(css, capsule_id, namespace) {
233
255
  cache_key = "css_#{capsule_id}_#{namespace}"
234
256
  should_cache = css.length > 100
235
257
  expires_at = Time.now + 1800
@@ -246,7 +268,7 @@ Writes CSS to files for HTTP caching. **Requires class method `def self.componen
246
268
  ```ruby
247
269
  class MyComponent < ApplicationComponent
248
270
  include StyleCapsule::Component
249
- stylesheet_registry cache_strategy: :file
271
+ style_capsule cache_strategy: :file
250
272
 
251
273
  # Must use class method for file caching
252
274
  def self.component_styles
@@ -265,10 +287,13 @@ StyleCapsule::CssFileWriter.configure(
265
287
  output_dir: Rails.root.join("app/assets/builds/capsules"),
266
288
  filename_pattern: ->(component_class, capsule_id) {
267
289
  "capsule-#{capsule_id}.css"
268
- }
290
+ },
291
+ fallback_dir: "/tmp/style_capsule" # Optional, defaults to /tmp/style_capsule
269
292
  )
270
293
  ```
271
294
 
295
+ **Fallback Directory:** In production environments where the app directory is read-only (e.g., Docker containers), StyleCapsule automatically falls back to writing files to `/tmp/style_capsule` when the default location is not writable. When using the fallback directory, the gem gracefully falls back to inline CSS rendering, keeping the UI fully functional.
296
+
272
297
  **Precompilation:**
273
298
 
274
299
  ```bash
@@ -280,6 +305,70 @@ Files are automatically built during `bin/rails assets:precompile`.
280
305
 
281
306
  **Compatibility:** The stylesheet registry works with Propshaft, Sprockets, and other Rails asset bundlers. Static file paths are collected in a process-wide manifest (similar to Propshaft's approach), while inline CSS is stored per-request.
282
307
 
308
+ ## Instrumentation
309
+
310
+ StyleCapsule provides comprehensive instrumentation via `ActiveSupport::Notifications` for monitoring CSS processing and file writing operations. All instrumentation is zero-overhead when no subscribers are present.
311
+
312
+ ### Available Events
313
+
314
+ - `style_capsule.css_processor.scope` - CSS scoping operations with duration and size metrics
315
+ - `style_capsule.css_file_writer.write` - CSS file write operations with duration and size metrics
316
+ - `style_capsule.css_file_writer.fallback` - When fallback directory is used (read-only filesystem)
317
+ - `style_capsule.css_file_writer.fallback_failure` - When both primary and fallback directories fail
318
+ - `style_capsule.css_file_writer.write_failure` - Other write errors
319
+
320
+ ### Example: Monitoring CSS Processing
321
+
322
+ ```ruby
323
+ # config/initializers/style_capsule.rb
324
+ ActiveSupport::Notifications.subscribe("style_capsule.css_processor.scope") do |name, start, finish, id, payload|
325
+ duration_ms = (finish - start) * 1000
326
+ Rails.logger.info "CSS scoped in #{duration_ms.round(2)}ms, input: #{payload[:input_size]} bytes, output: #{payload[:output_size]} bytes"
327
+ end
328
+ ```
329
+
330
+ ### Example: Monitoring File Writes
331
+
332
+ ```ruby
333
+ ActiveSupport::Notifications.subscribe("style_capsule.css_file_writer.write") do |name, start, finish, id, payload|
334
+ duration_ms = (finish - start) * 1000
335
+ StatsD.timing("style_capsule.write.duration", duration_ms)
336
+ StatsD.histogram("style_capsule.write.size", payload[:size])
337
+ end
338
+ ```
339
+
340
+ ### Example: Monitoring Fallback Scenarios
341
+
342
+ ```ruby
343
+ ActiveSupport::Notifications.subscribe("style_capsule.css_file_writer.fallback") do |name, start, finish, id, payload|
344
+ Rails.logger.warn "StyleCapsule fallback used: #{payload[:component_class]} -> #{payload[:fallback_path]}"
345
+ # Exception info available: payload[:exception] and payload[:exception_object]
346
+ StatsD.increment("style_capsule.css_file_writer.fallback", tags: [
347
+ "component:#{payload[:component_class]}",
348
+ "error:#{payload[:exception].first}"
349
+ ])
350
+ end
351
+ ```
352
+
353
+ ### Example: Error Reporting
354
+
355
+ ```ruby
356
+ ActiveSupport::Notifications.subscribe("style_capsule.css_file_writer.fallback_failure") do |name, start, finish, id, payload|
357
+ ActionReporter.notify(
358
+ "StyleCapsule: CSS write failure (both primary and fallback failed)",
359
+ context: {
360
+ component_class: payload[:component_class],
361
+ original_path: payload[:original_path],
362
+ fallback_path: payload[:fallback_path],
363
+ original_exception: payload[:original_exception],
364
+ fallback_exception: payload[:fallback_exception]
365
+ }
366
+ )
367
+ end
368
+ ```
369
+
370
+ For more details, see the [ActiveSupport::Notifications documentation](https://guides.rubyonrails.org/active_support_instrumentation.html).
371
+
283
372
  ## Advanced Usage
284
373
 
285
374
  ### Database-Stored CSS
data/SECURITY.md CHANGED
@@ -1,55 +1,28 @@
1
1
  # SECURITY
2
2
 
3
- ## Supported Versions
3
+ ## Reporting a Vulnerability
4
4
 
5
- | Version | Supported |
6
- | ------- | ------------------ |
7
- | 1.0.x | :white_check_mark: |
8
- | < 1.0 | :x: |
5
+ **Do NOT** open a public GitHub issue for security vulnerabilities.
9
6
 
10
- ## Security Guidelines
7
+ Email security details to: **security@kiskolabs.com**
11
8
 
12
- StyleCapsule provides minimal security protections to assist with automated CSS scoping. The gem does not validate, sanitize, or secure CSS content itself—this is the application's responsibility.
9
+ Include: description, steps to reproduce, potential impact, and suggested fix (if available).
13
10
 
14
- ### What StyleCapsule Protects
11
+ ### Response Timeline
15
12
 
16
- - **Path Traversal**: Filenames are validated when writing CSS files to prevent directory traversal attacks
17
- - **Input Size Limits**: CSS content is limited to 1MB per component to prevent resource exhaustion
18
- - **Scope ID Validation**: Capsule IDs are validated to prevent injection into HTML attributes
13
+ - We will acknowledge receipt of your report
14
+ - We will provide an initial assessment
15
+ - We will keep you informed of our progress and resolution timeline
19
16
 
20
- ### What StyleCapsule Does NOT Control
17
+ ### Disclosure Policy
21
18
 
22
- **Developer Responsibility:**
23
- - **CSS Content**: StyleCapsule does not validate or sanitize CSS content. Malicious CSS (e.g., data exfiltration via `@import`, CSS injection attacks) is not prevented by the gem
24
- - **User Input**: Applications must validate and sanitize user-provided CSS before passing it to StyleCapsule
25
- - **Developer Intent**: The gem trusts that developers provide safe CSS content from trusted sources
19
+ - We will work with you to understand and resolve the issue
20
+ - We will credit you for the discovery (unless you prefer to remain anonymous)
21
+ - We will publish a security advisory after the vulnerability is patched
22
+ - We will coordinate public disclosure with you
26
23
 
27
- **Rails Framework:**
28
- - HTML escaping is handled by Rails' built-in helpers (`content_tag`, etc.)
29
- - Content Security Policy (CSP) must be configured at the application level
30
- - File system permissions and access control are managed by the application
24
+ ## Automation Security
31
25
 
32
- ### Security Best Practices
26
+ * **Context Isolation:** It is strictly forbidden to include production credentials, API keys, or Personally Identifiable Information (PII) in prompts sent to third-party LLMs or automation services.
33
27
 
34
- 1. **Validate User Input**: Never pass untrusted CSS content to StyleCapsule without validation
35
- 2. **Use Content Security Policy**: Configure CSP headers to restrict inline styles and external resources
36
- 3. **Sanitize User-Generated CSS**: If allowing user input, sanitize CSS before processing
37
- 4. **Keep Dependencies Updated**: Use supported Ruby (>= 3.0) and Rails versions with security patches
38
- 5. **Review Generated Files**: Periodically review files in `app/assets/builds/capsules/` if using file-based caching
39
-
40
- ### Reporting a Vulnerability
41
-
42
- If you discover a security vulnerability, please **do not** open a public issue. Instead:
43
-
44
- 1. **Email**: contact@kiskolabs.com
45
- 2. **Subject**: `[SECURITY] style_capsule vulnerability report`
46
- 3. **Include**: Description, steps to reproduce, potential impact, and suggested fix (if any)
47
-
48
- We will acknowledge receipt within 48 hours and provide an initial assessment within 7 days.
49
-
50
- ### Security Updates
51
-
52
- Security updates are released as patch versions (e.g., 1.0.1, 1.0.2) and announced via:
53
- - GitHub Security Advisories
54
- - RubyGems release notes
55
- - CHANGELOG.md
28
+ * **Supply Chain:** All automated dependencies must be verified.
@@ -227,23 +227,104 @@ module StyleCapsule
227
227
  end
228
228
  end
229
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
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)
237
233
  def head_rendering?
238
- return false unless defined?(@head_rendering)
239
- @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
240
241
  end
241
242
 
242
243
  public :head_rendering?
243
244
 
244
- # Get the namespace for stylesheet registry
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)
245
248
  def stylesheet_namespace
246
- @stylesheet_namespace if defined?(@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
247
328
  end
248
329
 
249
330
  # Get the custom scope ID if set (alias for capsule_id getter)
@@ -251,22 +332,55 @@ module StyleCapsule
251
332
  @custom_capsule_id if defined?(@custom_capsule_id)
252
333
  end
253
334
 
254
- # Get inline cache strategy
335
+ # Get inline cache strategy (checks instance variable, then parent class, defaults to nil)
336
+ #
337
+ # @return [Symbol, nil] The cache strategy (default: nil)
255
338
  def inline_cache_strategy
256
- @inline_cache_strategy if defined?(@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
257
344
  end
258
345
 
259
- # Get inline cache TTL
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)
260
349
  def inline_cache_ttl
261
- @inline_cache_ttl if defined?(@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
262
355
  end
263
356
 
264
- # Get inline cache proc
357
+ # Get inline cache proc (checks instance variable, then parent class, defaults to nil)
358
+ #
359
+ # @return [Proc, nil] The cache proc (default: nil)
265
360
  def inline_cache_proc
266
- @inline_cache_proc if defined?(@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
267
366
  end
268
367
 
269
- public :head_rendering?, :stylesheet_namespace, :custom_capsule_id, :inline_cache_strategy, :inline_cache_ttl, :inline_cache_proc
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
270
384
 
271
385
  # Set or get options for stylesheet_link_tag when using file-based caching
272
386
  #
@@ -289,58 +403,6 @@ module StyleCapsule
289
403
  end
290
404
 
291
405
  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
344
406
  end
345
407
 
346
408
  # Module that wraps view_template to add scoped wrapper
@@ -463,9 +525,9 @@ module StyleCapsule
463
525
  # Use the configured scoping strategy
464
526
  scoped_css = case scoping_strategy
465
527
  when :nesting
466
- CssProcessor.scope_with_nesting(css_content, capsule_id)
528
+ CssProcessor.scope_with_nesting(css_content, capsule_id, component_class: self.class)
467
529
  else # :selector_patching (default)
468
- CssProcessor.scope_selectors(css_content, capsule_id)
530
+ CssProcessor.scope_selectors(css_content, capsule_id, component_class: self.class)
469
531
  end
470
532
 
471
533
  # Cache at class level (one style block per component type/scope/strategy combination)