scoped_css 0.1.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 +7 -0
- data/README.md +268 -0
- data/lib/scoped_css.rb +96 -0
- data/lib/version.rb +3 -0
- metadata +73 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4cc56b9cf9562506d46c2afe9e006ba1b97150e1451a47b6b9c908252bfdbfe6
|
4
|
+
data.tar.gz: de380cffe066073f85e6d661b9b2ccf29c17094aba9671085ee85965e0534206
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b89122d003f20e56b8fab45906a18fa24ca5fea48353db7bc43a6f4f1b967038863a6a69fa7f75489f779e0a622e6050efc3204982d6530173217bb39817a0ac
|
7
|
+
data.tar.gz: 3210446f837e4ec5900615b6d4a085e3f33aa5c1c652f08920f20edb3097af208635d48739981bb815b8942fe7332687fce1c61be13a989a546ee719a223b747
|
data/README.md
ADDED
@@ -0,0 +1,268 @@
|
|
1
|
+
# Scoped CSS
|
2
|
+
|
3
|
+
|
4
|
+
## Installation
|
5
|
+
|
6
|
+
1. Add to your Gemfile:
|
7
|
+
`gem 'scoped_css'`
|
8
|
+
2. Run `bundle install`
|
9
|
+
3. Include the helper in your views:
|
10
|
+
```ruby
|
11
|
+
module ApplicationHelper
|
12
|
+
include ScopedCss::Helper
|
13
|
+
# other helpers...
|
14
|
+
end
|
15
|
+
```
|
16
|
+
4. Use the helper in templates:
|
17
|
+
```erb
|
18
|
+
<% style_string, styles = scoped_css do %>
|
19
|
+
<style>
|
20
|
+
.header { font-weight: bold; }
|
21
|
+
.content { margin: 10px; }
|
22
|
+
<style>
|
23
|
+
<% end %>
|
24
|
+
|
25
|
+
<h1 class="<%= styles[:header] %>">Title</h1>
|
26
|
+
<main class="<%= styles[:content] %>">Content here</main>
|
27
|
+
|
28
|
+
<%= style_string %>
|
29
|
+
```
|
30
|
+
|
31
|
+
## Usage with ViewComponent
|
32
|
+
|
33
|
+
_app/components/section_component.rb_
|
34
|
+
```ruby
|
35
|
+
class SectionComponent < ViewComponent::Base
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
_app/components/section_component.html.erb_
|
40
|
+
```erb
|
41
|
+
<% style_string, styles = helpers.scoped_css do %>
|
42
|
+
<style>
|
43
|
+
.section {
|
44
|
+
border: none;
|
45
|
+
scroll-snap-align: center;
|
46
|
+
color: purple;
|
47
|
+
}
|
48
|
+
.heading {
|
49
|
+
font-size: 2rem;
|
50
|
+
}
|
51
|
+
</style>
|
52
|
+
<% end %>
|
53
|
+
|
54
|
+
<section class="<%= styles[:section] %>">
|
55
|
+
<h2 class="<%= styles[:heading] %>">Section</h2>
|
56
|
+
<%= content %>
|
57
|
+
</section>
|
58
|
+
|
59
|
+
<%= style_string %>
|
60
|
+
```
|
61
|
+
|
62
|
+
## Attribute Splatting
|
63
|
+
|
64
|
+
Sometimes you want to apply html attributes to a component from the parent template.
|
65
|
+
|
66
|
+
|
67
|
+
_app/components/section_component.rb_
|
68
|
+
```ruby
|
69
|
+
class SectionComponent < ViewComponent::Base
|
70
|
+
def initialize(attributes: {})
|
71
|
+
@attributes = attributes
|
72
|
+
end
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
_app/views/home/index.html.erb_
|
77
|
+
```erb
|
78
|
+
|
79
|
+
<% style_string, styles = scoped_css do %>
|
80
|
+
<style>
|
81
|
+
.section {
|
82
|
+
margin: 10px;
|
83
|
+
}
|
84
|
+
.heading {
|
85
|
+
font-size: 3rem;
|
86
|
+
}
|
87
|
+
</style>
|
88
|
+
<% end %>
|
89
|
+
|
90
|
+
<h1 class="<%= styles[:heading] %>">Title</h1>
|
91
|
+
<%= render SectionComponent.new(attributes: { id: "important-section", class: styles[:section] }) do %>
|
92
|
+
<p>Section 1</p>
|
93
|
+
<% end %>
|
94
|
+
|
95
|
+
<%= style_string %>
|
96
|
+
```
|
97
|
+
|
98
|
+
_app/components/section_component.html.erb_
|
99
|
+
```erb
|
100
|
+
<% style_string, styles = helpers.scoped_css do %>
|
101
|
+
<style>
|
102
|
+
.section {
|
103
|
+
border: none;
|
104
|
+
scroll-snap-align: center;
|
105
|
+
color: purple;
|
106
|
+
}
|
107
|
+
.heading {
|
108
|
+
font-size: 2rem;
|
109
|
+
}
|
110
|
+
</style>
|
111
|
+
<% end %>
|
112
|
+
|
113
|
+
<section <%= helpers.splat_attributes(@attributes, styles[:section]) %>>
|
114
|
+
<h2 class="<%= styles[:heading] %>">Section</h2>
|
115
|
+
<%= content %>
|
116
|
+
</section>
|
117
|
+
|
118
|
+
<%= style_string %>
|
119
|
+
```
|
120
|
+
|
121
|
+
:info: Note that `<section class="<%= styles[:heading] %>" <%= helpers.splat_attributes(@attributes, styles[:section]) %>>` is not used. Instead, the `splat_attributes` helper is used to apply the class attribute to the component. The `splat_attributes` helper takes CSS class names after the attributes argument and concatenates these CSS class names with any class names in the attributes hash.
|
122
|
+
|
123
|
+
This will generate the following HTML:
|
124
|
+
|
125
|
+
```html
|
126
|
+
<!-- app/views/home/index.html.erb -->
|
127
|
+
|
128
|
+
<h1 class=".atge5q2e-heading">Title</h1>
|
129
|
+
|
130
|
+
<!-- app/components/section_component.html.erb -->
|
131
|
+
|
132
|
+
<section class="agkd94j4-section atge5q2e-section" id="important-section">
|
133
|
+
<h2 class="agkd94j4-heading">
|
134
|
+
</section>
|
135
|
+
|
136
|
+
<section class="agkd94j4-section">
|
137
|
+
<h2 class="agkd94j4-heading">
|
138
|
+
<p>Section</p>
|
139
|
+
</section>
|
140
|
+
|
141
|
+
<style>
|
142
|
+
.agkd94j4-section {
|
143
|
+
border: none;
|
144
|
+
scroll-snap-align: center;
|
145
|
+
color: purple;
|
146
|
+
}
|
147
|
+
.agkd94j4-heading {
|
148
|
+
font-size: 2rem;
|
149
|
+
}
|
150
|
+
</style>
|
151
|
+
|
152
|
+
<!-- app/views/home/index.html.erb -->
|
153
|
+
|
154
|
+
<style>
|
155
|
+
.atge5q2e-section {
|
156
|
+
margin: 10px;
|
157
|
+
}
|
158
|
+
.atge5q2e-heading {
|
159
|
+
font-size: 3rem;
|
160
|
+
}
|
161
|
+
</style>
|
162
|
+
|
163
|
+
```
|
164
|
+
|
165
|
+
### CSS Specificity
|
166
|
+
|
167
|
+
In previous examples, we applied the CSS class `.section` to the `<section>` element. It has the property `color: purple;`. But what if we want one instance of the SectionComponent to have a different color? In the parent template we can define a CSS class for the section component with the different color and apply it to the component.
|
168
|
+
|
169
|
+
_app/components/section_component.rb_
|
170
|
+
```ruby
|
171
|
+
class SectionComponent < ViewComponent::Base
|
172
|
+
def initialize(attributes: {})
|
173
|
+
@attributes = attributes
|
174
|
+
end
|
175
|
+
end
|
176
|
+
```
|
177
|
+
|
178
|
+
_app/components/section_component.html.erb_
|
179
|
+
```erb
|
180
|
+
<% style_string, styles = helpers.scoped_css do %>
|
181
|
+
<style>
|
182
|
+
.section {
|
183
|
+
border: none;
|
184
|
+
scroll-snap-align: center;
|
185
|
+
color: purple;
|
186
|
+
}
|
187
|
+
.heading {
|
188
|
+
font-size: 2rem;
|
189
|
+
}
|
190
|
+
</style>
|
191
|
+
<% end %>
|
192
|
+
|
193
|
+
<section <%= helpers.splat_attributes(@attributes, styles[:section]) %>>
|
194
|
+
<h2 class="<%= styles[:heading] %>">Section</h2>
|
195
|
+
<%= content %>
|
196
|
+
</section>
|
197
|
+
|
198
|
+
<%= style_string %>
|
199
|
+
```
|
200
|
+
|
201
|
+
_app/views/home/index.html.erb_
|
202
|
+
```erb
|
203
|
+
<% style_string, styles = scoped_css do %>
|
204
|
+
<style>
|
205
|
+
.section {
|
206
|
+
margin: 10px;
|
207
|
+
color: darkgreen;
|
208
|
+
}
|
209
|
+
.heading {
|
210
|
+
font-size: 3rem;
|
211
|
+
}
|
212
|
+
</style>
|
213
|
+
<% end %>
|
214
|
+
|
215
|
+
<h1 class="<%= styles[:heading] %>">Title</h1>
|
216
|
+
<%= render SectionComponent.new(attributes: { class: styles[:section] }) do %>
|
217
|
+
<p>Section 1</p>
|
218
|
+
<% end %>
|
219
|
+
|
220
|
+
<%= render SectionComponent.new() do %>
|
221
|
+
<p>Section 2</p>
|
222
|
+
<% end %>
|
223
|
+
|
224
|
+
<%= style_string %>
|
225
|
+
```
|
226
|
+
|
227
|
+
The reason this works is because the we render the style_string in each template at the bottom of the template. Any nested components style tag will be rendered before the parent style tag. The last declared selector has precedence. And because a scoped CSS class name is passed to the component only that instance of the component will use the new color.
|
228
|
+
|
229
|
+
```html
|
230
|
+
<!-- app/views/home/index.html.erb -->
|
231
|
+
|
232
|
+
<h1 class=".atge5q2e-heading">Title</h1>
|
233
|
+
|
234
|
+
<!-- app/components/section_component.html.erb -->
|
235
|
+
|
236
|
+
<section class="agkd94j4-section atge5q2e-section">
|
237
|
+
<h2 class="agkd94j4-heading">
|
238
|
+
<p>Section 1</p>
|
239
|
+
</section>
|
240
|
+
|
241
|
+
<section class="agkd94j4-section atge5q2e-section">
|
242
|
+
<h2 class="agkd94j4-heading">
|
243
|
+
<p>Section 2</p>
|
244
|
+
</section>
|
245
|
+
|
246
|
+
<style>
|
247
|
+
.agkd94j4-section {
|
248
|
+
border: none;
|
249
|
+
scroll-snap-align: center;
|
250
|
+
color: purple;
|
251
|
+
}
|
252
|
+
.agkd94j4-heading {
|
253
|
+
font-size: 2rem;
|
254
|
+
}
|
255
|
+
</style>
|
256
|
+
|
257
|
+
<!-- app/views/home/index.html.erb -->
|
258
|
+
|
259
|
+
<style>
|
260
|
+
.atge5q2e-section {
|
261
|
+
margin: 10px;
|
262
|
+
color: darkgreen;
|
263
|
+
}
|
264
|
+
.atge5q2e-heading {
|
265
|
+
font-size: 3rem;
|
266
|
+
}
|
267
|
+
</style>
|
268
|
+
```
|
data/lib/scoped_css.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require_relative "version"
|
2
|
+
require "digest/sha2"
|
3
|
+
require "erb"
|
4
|
+
|
5
|
+
module ScopedCss
|
6
|
+
module Helper
|
7
|
+
def scoped_css(&css_block)
|
8
|
+
@per_template_outputs ||= Hash.new
|
9
|
+
|
10
|
+
css_block_content = ""
|
11
|
+
if block_given?
|
12
|
+
css_block_content = capture(&css_block)
|
13
|
+
end
|
14
|
+
|
15
|
+
prefix = "a#{Digest::SHA1.hexdigest(css_block_content)}"[0,8]
|
16
|
+
|
17
|
+
styles = Hash.new
|
18
|
+
prefixed_css_block_content = Rails.env.local? ? ' <!-- previously output --> ' : ''
|
19
|
+
|
20
|
+
if @per_template_outputs.has_key?(prefix)
|
21
|
+
styles = @per_template_outputs[prefix]
|
22
|
+
else
|
23
|
+
prefixed_css_block_content, styles = prefix_css_classes(css_block_content, prefix)
|
24
|
+
@per_template_outputs[prefix] = styles
|
25
|
+
end
|
26
|
+
|
27
|
+
result = prefixed_css_block_content.respond_to?(:html_safe) ? prefixed_css_block_content.html_safe : prefixed_css_block_content
|
28
|
+
return [result, styles, prefix]
|
29
|
+
end
|
30
|
+
|
31
|
+
# Helper to take attribute hashes (or strings for classes) and format them
|
32
|
+
# into a string suitable for direct HTML attribute "splatting".
|
33
|
+
#
|
34
|
+
# Example usage in ERB:
|
35
|
+
# <section <%= splat_attributes(@attributes, styles[:section]) %>>
|
36
|
+
# <%= content %>
|
37
|
+
# </section>
|
38
|
+
#
|
39
|
+
# @param args [Array<Hash, String>] One or more attribute hashes or class strings
|
40
|
+
# @return [String] A string of HTML attributes (e.g., 'class="foo bar" id="my-id"')
|
41
|
+
def splat_attributes(*args)
|
42
|
+
combined_attributes = merge_classes(*args)
|
43
|
+
|
44
|
+
# Convert the combined hash into an HTML attribute string
|
45
|
+
result = combined_attributes.map do |key, value|
|
46
|
+
html_key = key.to_s.gsub('_', '-')
|
47
|
+
escaped_value = ERB::Util.html_escape(value.to_s)
|
48
|
+
|
49
|
+
# Handle boolean attributes (e.g., 'disabled' instead of 'disabled="true"')
|
50
|
+
if value.is_a?(TrueClass) && !html_key.empty?
|
51
|
+
html_key
|
52
|
+
elsif value.is_a?(FalseClass)
|
53
|
+
# Don't render attributes that are explicitly false
|
54
|
+
nil
|
55
|
+
elsif !escaped_value.empty?
|
56
|
+
"#{html_key}=\"#{escaped_value}\""
|
57
|
+
else
|
58
|
+
nil # Don't render attributes with empty values
|
59
|
+
end
|
60
|
+
end.compact.join(" ").strip
|
61
|
+
|
62
|
+
result.respond_to?(:html_safe) ? result.html_safe : result
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def prefix_css_classes(css_string, prefix)
|
68
|
+
updated_css_string = css_string.dup
|
69
|
+
class_name_map = {}
|
70
|
+
class_selector_regex = /\.([_a-zA-Z][_a-zA-Z0-9-]*)/
|
71
|
+
|
72
|
+
updated_css_string.gsub!(class_selector_regex) do |full_match|
|
73
|
+
original_class_name = Regexp.last_match[1]
|
74
|
+
prefixed_class_name = "#{prefix}-#{original_class_name}"
|
75
|
+
class_name_map[original_class_name.to_sym] = prefixed_class_name
|
76
|
+
".#{prefixed_class_name}"
|
77
|
+
end
|
78
|
+
|
79
|
+
return updated_css_string, class_name_map
|
80
|
+
end
|
81
|
+
|
82
|
+
def merge_classes(attributes, *css_classes)
|
83
|
+
merged_attributes = attributes.dup
|
84
|
+
|
85
|
+
css_class_string = css_classes.compact.join(" ").strip
|
86
|
+
|
87
|
+
if merged_attributes[:class].nil? || merged_attributes[:class].empty?
|
88
|
+
merged_attributes[:class] = css_class_string
|
89
|
+
else
|
90
|
+
merged_attributes[:class] = "#{css_class_string} #{merged_attributes[:class]}".strip
|
91
|
+
end
|
92
|
+
|
93
|
+
merged_attributes
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: scoped_css
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ewan McDougall
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-06-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: Scope CSS to templates and ViewComponents
|
42
|
+
email: ewan@mrloop.com
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- README.md
|
48
|
+
- lib/scoped_css.rb
|
49
|
+
- lib/version.rb
|
50
|
+
homepage:
|
51
|
+
licenses:
|
52
|
+
- MIT
|
53
|
+
metadata: {}
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
requirements: []
|
69
|
+
rubygems_version: 3.5.22
|
70
|
+
signing_key:
|
71
|
+
specification_version: 4
|
72
|
+
summary: Scoped CSS
|
73
|
+
test_files: []
|