view_component-scoped_styles 0.6.0 → 0.7.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/README.md +56 -16
- data/lib/view_component/scoped_styles/component_generator_hook.rb +1 -0
- data/lib/view_component/scoped_styles/concern.rb +14 -32
- data/lib/view_component/scoped_styles/css_selectors.rb +151 -0
- data/lib/view_component/scoped_styles/railtie.rb +1 -1
- data/lib/view_component/scoped_styles/version.rb +1 -1
- data/lib/view_component/scoped_styles.rb +2 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 76cfb539bacc159bd6913d32e02fd65f4bdf694e94956fdb9187b68b951badbe
|
|
4
|
+
data.tar.gz: 6cd273fa833779ed196785c3318ffb79c3c55929c9637aa067d6d2e97a1bbb14
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b7ebb5bd1bbba3ad26b14119f80d54623a0aae174956c178600c130106d23517053ab232b91ba4f516cebca07fb7c08581a34ad4cab83a6cce00c611957110a3
|
|
7
|
+
data.tar.gz: 0caa033226552ccf3ea15d7a6d8af1240449d37a68d1c18f2cb549882c46ee42145cf0f41434e28af4fe68fe242337c3f4b75af27ee93a917412f337e1c32a98
|
data/README.md
CHANGED
|
@@ -15,7 +15,7 @@ E.g. `.button` becomes `.button_a1b2c3d4`
|
|
|
15
15
|
- [1. Using a sidecar stylesheet](#1-using-a-sidecar-stylesheet)
|
|
16
16
|
- [2. Using a styles block in the component](#2-using-a-styles-block-in-the-component)
|
|
17
17
|
- [Referencing classes](#referencing-classes)
|
|
18
|
-
- [
|
|
18
|
+
- [Global classes](#global-classes)
|
|
19
19
|
- [Using the scoped CSS](#using-the-scoped-css)
|
|
20
20
|
- [Configuration](#configuration)
|
|
21
21
|
- [Related projects](#related-projects)
|
|
@@ -248,33 +248,73 @@ end
|
|
|
248
248
|
|
|
249
249
|
**Upgrading from 0.4.x:** set `config.css_class_prefix = "c-"` (and per-component overrides) to keep the previous `c-a1b2c3d4` class names without updating templates.
|
|
250
250
|
|
|
251
|
-
###
|
|
251
|
+
### Global classes
|
|
252
252
|
|
|
253
|
-
|
|
253
|
+
Use CSS Modules-style `:global(...)` to leave class selectors unchanged in a sidecar stylesheet or `styles` block:
|
|
254
254
|
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
|
|
255
|
+
```css
|
|
256
|
+
.component:global(.is-open) {
|
|
257
|
+
display: block;
|
|
258
|
+
}
|
|
258
259
|
|
|
259
|
-
|
|
260
|
+
:global(.active) .label {
|
|
261
|
+
font-weight: bold;
|
|
262
|
+
}
|
|
263
|
+
```
|
|
260
264
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
265
|
+
The compiler removes `:global(...)` and leaves its contents unscoped. In these examples, `.component` and `.label` get scoped names, while `.is-open` and `.active` stay unchanged. Nested selector functions work too:
|
|
266
|
+
|
|
267
|
+
```css
|
|
268
|
+
.component:has(:global(.external:not(.disabled))) {
|
|
269
|
+
border-color: green;
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Bare `:global` makes the rest of the current selector global. Use `:local(...)` for a local selector within it, or bare `:local` to switch back. Scope resets at each comma, at the end of a selector function, and for each new rule.
|
|
274
|
+
|
|
275
|
+
```css
|
|
276
|
+
.component :global .external .icon :local(.label) {
|
|
277
|
+
color: red;
|
|
278
|
+
}
|
|
268
279
|
```
|
|
269
|
-
|
|
280
|
+
|
|
281
|
+
For selector lists, write `:global(.first), :global(.second)`. Lists inside a single `:global(...)` are not supported.
|
|
282
|
+
|
|
283
|
+
Global scope applies to each occurrence. A class can be global in one selector and local in another. `component_class("name")` returns the scoped name when there is a local occurrence, or the original name when every occurrence is global. The root helper uses the configured root class when present, otherwise the first local class, or the first global class if there are no local classes.
|
|
284
|
+
|
|
285
|
+
In your view, reference a global class directly:
|
|
286
|
+
|
|
270
287
|
```erb
|
|
271
288
|
<div class="<%= component_class %> is-open">
|
|
272
289
|
```
|
|
273
|
-
|
|
290
|
+
|
|
291
|
+
Or use the helper for a class that appears only in global scope:
|
|
292
|
+
|
|
274
293
|
```erb
|
|
275
294
|
<div class="<%= component_class %> <%= component_class("is-open") %>">
|
|
276
295
|
```
|
|
277
296
|
|
|
297
|
+
#### Migrating from ignored_css_classes
|
|
298
|
+
|
|
299
|
+
`ignored_css_classes` is deprecated and emits an ActiveSupport deprecation warning. It continues to leave every occurrence of the listed classes unchanged, including their helper results.
|
|
300
|
+
|
|
301
|
+
Remove the Ruby declaration and wrap each occurrence of those classes in `:global(...)`:
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
# Remove this from your component:
|
|
305
|
+
ignored_css_classes "is-open", "active"
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
```css
|
|
309
|
+
/* Before */
|
|
310
|
+
.component.is-open .active { color: red; }
|
|
311
|
+
|
|
312
|
+
/* After */
|
|
313
|
+
.component:global(.is-open) :global(.active) { color: red; }
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Regenerate the bundled stylesheet after migrating. Scoped names are content-derived, so changing the CSS changes their hashes.
|
|
317
|
+
|
|
278
318
|
### Using the scoped CSS
|
|
279
319
|
|
|
280
320
|
All scoped CSS will be compiled into a single bundled stylesheet. By default that is `app/assets/stylesheets/components.scoped.css`; both the directory and filename are configurable (see [Configuration](#configuration)).
|
|
@@ -7,7 +7,7 @@ module ViewComponent
|
|
|
7
7
|
extend ActiveSupport::Concern
|
|
8
8
|
|
|
9
9
|
CACHED_VARIABLES = %i[@component_styles @component_id @component_class_map].freeze
|
|
10
|
-
|
|
10
|
+
DEPRECATOR = ActiveSupport::Deprecation.new("a future release", "ViewComponent::ScopedStyles")
|
|
11
11
|
COMPONENT_REFERENCE_PATTERN = /
|
|
12
12
|
:component\(
|
|
13
13
|
\s*
|
|
@@ -52,7 +52,12 @@ module ViewComponent
|
|
|
52
52
|
# Clears cached generated styles when the list changes.
|
|
53
53
|
#
|
|
54
54
|
# @param classes [String, Symbol] selector names without a leading dot
|
|
55
|
+
# @deprecated Wrap selectors in +:global(...)+ in the stylesheet instead.
|
|
55
56
|
def ignored_css_classes(*classes)
|
|
57
|
+
DEPRECATOR.warn(
|
|
58
|
+
"ignored_css_classes is deprecated. Use :global(...) in your CSS instead, " \
|
|
59
|
+
"for example :global(.active)."
|
|
60
|
+
)
|
|
56
61
|
if classes.any?
|
|
57
62
|
names = classes.flatten.map { |css_class| css_class.to_s.delete_prefix(".") }
|
|
58
63
|
const_set(:IGNORED_CSS_CLASSES, names.freeze)
|
|
@@ -134,13 +139,15 @@ module ViewComponent
|
|
|
134
139
|
component_resolution_stack.push(self)
|
|
135
140
|
pushed = true
|
|
136
141
|
styles_content = generate_styles_content
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
selectors = CssSelectors.new(styles_content)
|
|
143
|
+
css_classes = selectors.local_classes
|
|
144
|
+
primary_class = primary_css_class(css_classes + selectors.global_classes)
|
|
139
145
|
|
|
140
|
-
@component_id = component_id_for(primary_class, styles_content)
|
|
141
146
|
@component_class_map = build_component_class_map(styles_content, css_classes, primary_class)
|
|
147
|
+
selectors.global_classes.each { |name| @component_class_map[name] ||= name }
|
|
148
|
+
@component_id = @component_class_map[primary_class]
|
|
142
149
|
@component_styles = replace_component_references(
|
|
143
|
-
|
|
150
|
+
selectors.render(@component_class_map)
|
|
144
151
|
)
|
|
145
152
|
ensure
|
|
146
153
|
component_resolution_stack.pop if pushed
|
|
@@ -150,10 +157,6 @@ module ViewComponent
|
|
|
150
157
|
@styles_block ? @styles_block.call : File.read(stylesheet_path)
|
|
151
158
|
end
|
|
152
159
|
|
|
153
|
-
def extract_css_classes(styles_content)
|
|
154
|
-
styles_content.scan(CLASS_SELECTOR_PATTERN).flatten.uniq
|
|
155
|
-
end
|
|
156
|
-
|
|
157
160
|
def primary_css_class(css_classes)
|
|
158
161
|
configured = self::COMPONENT_CSS_CLASS.delete_prefix(".")
|
|
159
162
|
css_classes.include?(configured) ? configured : css_classes.first
|
|
@@ -169,20 +172,6 @@ module ViewComponent
|
|
|
169
172
|
end
|
|
170
173
|
end
|
|
171
174
|
|
|
172
|
-
def replace_css_classes(styles_content, class_map)
|
|
173
|
-
scoped_map = class_map.reject do |css_class, scoped|
|
|
174
|
-
css_class == scoped
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
sorted_classes = scoped_map.keys.sort_by(&:length).reverse
|
|
178
|
-
|
|
179
|
-
sorted_classes.reduce(styles_content) do |content, css_class|
|
|
180
|
-
scoped = class_map[css_class]
|
|
181
|
-
escaped = CssClassPrefix.escape_for_css_selector(scoped)
|
|
182
|
-
content.gsub(/\.#{Regexp.escape(css_class)}\b/, ".#{escaped}")
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
|
|
186
175
|
def replace_component_references(styles_content)
|
|
187
176
|
styles_content.gsub(COMPONENT_REFERENCE_PATTERN) do
|
|
188
177
|
component_name = Regexp.last_match[:component_name]
|
|
@@ -194,14 +183,6 @@ module ViewComponent
|
|
|
194
183
|
end
|
|
195
184
|
end
|
|
196
185
|
|
|
197
|
-
def component_id_for(primary_class, styles_content)
|
|
198
|
-
if ignored_css_class?(primary_class)
|
|
199
|
-
primary_class
|
|
200
|
-
else
|
|
201
|
-
generate_scoped_class_id(styles_content, primary_class, primary_class)
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
|
|
205
186
|
def ignored_css_class?(css_class)
|
|
206
187
|
self::IGNORED_CSS_CLASSES.include?(css_class)
|
|
207
188
|
end
|
|
@@ -275,7 +256,8 @@ module ViewComponent
|
|
|
275
256
|
# Scoped CSS class for a selector (e.g. +"c-99d08d5a"+).
|
|
276
257
|
#
|
|
277
258
|
# With no argument, returns the scoped root class ({COMPONENT_CSS_CLASS} when it
|
|
278
|
-
# appears in the CSS, otherwise the first class
|
|
259
|
+
# appears in the CSS, otherwise the first local class, or the first global
|
|
260
|
+
# class when the stylesheet has no local classes).
|
|
279
261
|
#
|
|
280
262
|
# @param name [String, Symbol] CSS class without a leading dot (e.g. +"input-box"+)
|
|
281
263
|
# @param from [Class] component class to read scoped CSS classes from
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "strscan"
|
|
4
|
+
|
|
5
|
+
module ViewComponent
|
|
6
|
+
module ScopedStyles
|
|
7
|
+
# Records each class occurrence with its scope, preserving non-selector CSS.
|
|
8
|
+
class CssSelectors
|
|
9
|
+
COMMENT = %r{/\*.*?\*/}m
|
|
10
|
+
PROTECTED = %r{/\*.*?\*/|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\\.}m
|
|
11
|
+
CLASS_SELECTOR = /\.([a-zA-Z_][\w-]*)\b/
|
|
12
|
+
ClassSelector = Struct.new(:name, :local)
|
|
13
|
+
|
|
14
|
+
attr_reader :local_classes, :global_classes
|
|
15
|
+
|
|
16
|
+
def initialize(css)
|
|
17
|
+
@local_classes = []
|
|
18
|
+
@global_classes = []
|
|
19
|
+
@parts = parse_stylesheet(css)
|
|
20
|
+
@local_classes.uniq!
|
|
21
|
+
@global_classes.uniq!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def render(class_map)
|
|
25
|
+
@parts.map do |part|
|
|
26
|
+
if part.is_a?(ClassSelector)
|
|
27
|
+
name = part.local ? class_map.fetch(part.name) : part.name
|
|
28
|
+
".#{CssClassPrefix.escape_for_css_selector(name)}"
|
|
29
|
+
else
|
|
30
|
+
part
|
|
31
|
+
end
|
|
32
|
+
end.join
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# A rule's prelude ends at an opening brace. Semicolons and closing
|
|
38
|
+
# braces flush declarations without interpreting them as selectors.
|
|
39
|
+
def parse_stylesheet(css)
|
|
40
|
+
scanner = StringScanner.new(css)
|
|
41
|
+
parts = []
|
|
42
|
+
prelude = +""
|
|
43
|
+
depth = 0
|
|
44
|
+
|
|
45
|
+
until scanner.eos?
|
|
46
|
+
if (literal = scanner.scan(PROTECTED))
|
|
47
|
+
prelude << literal
|
|
48
|
+
next
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
char = scanner.getch
|
|
52
|
+
case char
|
|
53
|
+
when "(", "["
|
|
54
|
+
depth += 1
|
|
55
|
+
when ")", "]"
|
|
56
|
+
depth -= 1
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if depth.zero? && "{};".include?(char)
|
|
60
|
+
if char == "{"
|
|
61
|
+
parts.concat(parse_prelude(prelude))
|
|
62
|
+
else
|
|
63
|
+
parts << prelude
|
|
64
|
+
end
|
|
65
|
+
parts << char
|
|
66
|
+
prelude = +""
|
|
67
|
+
else
|
|
68
|
+
prelude << char
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
parts << prelude
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def parse_prelude(prelude)
|
|
76
|
+
rule = prelude.gsub(COMMENT, "").lstrip
|
|
77
|
+
scanner = StringScanner.new(prelude)
|
|
78
|
+
return parse_selector(scanner) unless rule.start_with?("@") && !rule.match?(/\A@scope\b/)
|
|
79
|
+
return [prelude] unless rule.match?(/\A@supports\b/)
|
|
80
|
+
|
|
81
|
+
parts = []
|
|
82
|
+
until scanner.eos?
|
|
83
|
+
if (literal = scanner.scan(PROTECTED))
|
|
84
|
+
parts << literal
|
|
85
|
+
elsif scanner.scan(/\bselector\(/)
|
|
86
|
+
parts << "selector("
|
|
87
|
+
parts.concat(parse_selector(scanner, closing: ")"))
|
|
88
|
+
parts << ")"
|
|
89
|
+
else
|
|
90
|
+
parts << scanner.getch
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
parts
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def parse_selector(scanner, local: true, closing: nil, scope_function: false)
|
|
97
|
+
parts = []
|
|
98
|
+
initial_scope = local
|
|
99
|
+
|
|
100
|
+
until scanner.eos?
|
|
101
|
+
if closing && scanner.scan(/#{Regexp.escape(closing)}/)
|
|
102
|
+
return parts
|
|
103
|
+
elsif (literal = scanner.scan(PROTECTED))
|
|
104
|
+
parts << literal
|
|
105
|
+
elsif scanner.scan(/\[/)
|
|
106
|
+
parts << "[" << read_attribute(scanner)
|
|
107
|
+
elsif scanner.scan(/:(global|local)\(/)
|
|
108
|
+
scope = scanner[1] == "local"
|
|
109
|
+
parts.concat(parse_selector(scanner, local: scope, closing: ")", scope_function: true))
|
|
110
|
+
elsif scanner.scan(/:(global|local)(?=\s)/)
|
|
111
|
+
local = scanner[1] == "local"
|
|
112
|
+
scanner.scan(/\s+/)
|
|
113
|
+
elsif scanner.scan(CLASS_SELECTOR)
|
|
114
|
+
name = scanner[1]
|
|
115
|
+
(local ? @local_classes : @global_classes) << name
|
|
116
|
+
parts << ClassSelector.new(name, local)
|
|
117
|
+
elsif scanner.scan(/\(/)
|
|
118
|
+
parts << "("
|
|
119
|
+
parts.concat(parse_selector(scanner, local: local, closing: ")"))
|
|
120
|
+
parts << ")"
|
|
121
|
+
elsif scanner.scan(/,/)
|
|
122
|
+
raise ArgumentError, "Use a separate :global(...) or :local(...) for each selector" if scope_function
|
|
123
|
+
|
|
124
|
+
local = initial_scope
|
|
125
|
+
parts << ","
|
|
126
|
+
else
|
|
127
|
+
parts << scanner.getch
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
raise ArgumentError, "Unclosed selector function in scoped CSS" if closing
|
|
132
|
+
|
|
133
|
+
parts
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def read_attribute(scanner)
|
|
137
|
+
value = +""
|
|
138
|
+
until scanner.eos?
|
|
139
|
+
if (literal = scanner.scan(PROTECTED))
|
|
140
|
+
value << literal
|
|
141
|
+
else
|
|
142
|
+
char = scanner.getch
|
|
143
|
+
value << char
|
|
144
|
+
break if char == "]"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
value
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
require_relative "scoped_styles/version"
|
|
4
4
|
require_relative "scoped_styles/configuration"
|
|
5
5
|
require_relative "scoped_styles/css_class_prefix"
|
|
6
|
+
require_relative "scoped_styles/css_selectors"
|
|
6
7
|
require "active_support/concern"
|
|
8
|
+
require "active_support/deprecation"
|
|
7
9
|
require "active_support/core_ext/enumerable"
|
|
8
10
|
require_relative "scoped_styles/concern"
|
|
9
11
|
require_relative "scoped_styles/stylist"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: view_component-scoped_styles
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chris Edwards
|
|
@@ -119,6 +119,7 @@ files:
|
|
|
119
119
|
- lib/view_component/scoped_styles/concern.rb
|
|
120
120
|
- lib/view_component/scoped_styles/configuration.rb
|
|
121
121
|
- lib/view_component/scoped_styles/css_class_prefix.rb
|
|
122
|
+
- lib/view_component/scoped_styles/css_selectors.rb
|
|
122
123
|
- lib/view_component/scoped_styles/railtie.rb
|
|
123
124
|
- lib/view_component/scoped_styles/stylist.rb
|
|
124
125
|
- lib/view_component/scoped_styles/stylist/writer.rb
|