view_component_css_dsl 0.1.4 → 0.1.5
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 +30 -0
- data/README.md +2 -1
- data/lib/view_component_css_dsl/verifier.rb +91 -1
- data/lib/view_component_css_dsl/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8881683c0d397b134f9fc656eda692df513d6bd3160c23ef397a212f51cd5eee
|
|
4
|
+
data.tar.gz: 58f07649276631f545c464dc758df825252ef1aeecd03524266528a886a3d9ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 202032a8ca4d109eab58eb325d5fd3f022479068654e4ab9d801ec515461f41d8d7fc359a4995bde466db2ccfcafc0ddde3fdcce8efe0f98924e8c3cd2b6af30
|
|
7
|
+
data.tar.gz: d3b1162aaac4717d3aef5b26e42b615ad3a642deb80fb386cf53f50538210e876271bd94e40df7f19302182893800b867582fb2cc34c16365c24805e4f350618
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
## [0.1.5] - 2026-06-09
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- ViewComponentCssDsl::Verifier cross_declaration_conflicts check — warns when a class declared in one place (e.g. a base `leading-snug`) is silently dropped because a *different* declaration merged on top of it (e.g. a size axis's `text-sm`, since Tailwind font-size utilities also set line-height). Suppresses intentional same-family overrides (`p-2` → `p-8`) and surfaces only cross-family drops.
|
|
10
|
+
- Verifier cross_declaration_conflicts check warning on classes silently dropped when separate DSL declarations merge (168a79e)
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- CHANGELOG.md retains the full release history (old entries were trimmed on every version bump) (793d53e)
|
|
15
|
+
|
|
5
16
|
## [0.1.4] - 2026-06-04
|
|
6
17
|
|
|
7
18
|
### Added
|
|
@@ -17,3 +28,22 @@
|
|
|
17
28
|
### Breaking
|
|
18
29
|
|
|
19
30
|
- `hidden` is now treated as part of the display-class conflict group. Strings like `"flex hidden"` collapse to `"hidden"` rather than keeping both. Previous behavior was non-standard — `tailwind-merge` (JS) and `tailwind_merge` (Ruby) have always treated `hidden` as a display utility. For JS-toggle visibility patterns, use the HTML5 `hidden` attribute (e.g., `attribute hidden: -> { … }` or pass `hidden: true` as an html_attr) and toggle it with `element.toggleAttribute('hidden')` instead of relying on the class merger.
|
|
31
|
+
|
|
32
|
+
## [0.1.2] - 2026-05-15
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- data, aria, and attribute DSL declarators for first-class HTML attribute declarations (cf37050)
|
|
37
|
+
|
|
38
|
+
## [0.1.1] - 2026-05-15
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- `view_component` dependency pinned to `~> 4.0` (was `>= 4.0`) — RubyGems-recommended SemVer-aware constraint
|
|
43
|
+
- Minimum Ruby version raised to 3.2 (was 3.1), matching the floor for `view_component >= 4.0`
|
|
44
|
+
|
|
45
|
+
## [0.1.0] - 2026-05-15
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- Initial release. Extracted from SOFware/forge.
|
data/README.md
CHANGED
|
@@ -462,12 +462,13 @@ Axis, method, and proc rules are appended, not overridden.
|
|
|
462
462
|
|
|
463
463
|
The DSL's worst failure modes are silent: a typo'd or hallucinated Tailwind class produces no CSS at all under JIT, a self-conflicting declaration quietly drops a class, and a rule referencing a missing method only raises at render time on the code path that hits it. `ViewComponentCssDsl::Verifier` catches all of these statically — fast enough to run on every edit.
|
|
464
464
|
|
|
465
|
-
`verify(component)` returns `Finding` structs (`component`, `check`, `severity`, `message`).
|
|
465
|
+
`verify(component)` returns `Finding` structs (`component`, `check`, `severity`, `message`). Seven checks run:
|
|
466
466
|
|
|
467
467
|
| Check | Asserts | Catches |
|
|
468
468
|
| --- | --- | --- |
|
|
469
469
|
| `class_validity` | Every declared class exists in the compiled Tailwind output | Typos, hallucinated classes, theme values that don't exist |
|
|
470
470
|
| `self_conflicts` | No declaration conflicts with itself | `css "block flex"` silently dropping `block` |
|
|
471
|
+
| `cross_declaration_conflicts` | No class declared in one place is silently dropped when a different declaration merges on top of it | A base `leading-snug` that a size axis's `text-sm` overrides (font-size utilities also set line-height) |
|
|
471
472
|
| `method_rules` | Every Symbol in `css`/`data`/`aria`/`attribute` rules resolves to a method | Render-time `NoMethodError`s |
|
|
472
473
|
| `axes_settable` | Every axis has an initialize param or `@ivar` assignment | Variant rules that can never fire |
|
|
473
474
|
| `variant_matrix` | `#css` builds cleanly for every axis-value combination | Anything the static checks miss, without rendering |
|
|
@@ -6,13 +6,17 @@ require "view_component_css_dsl"
|
|
|
6
6
|
# itself can't surface until render time (or surfaces silently). Designed to be
|
|
7
7
|
# fast enough to run on every edit.
|
|
8
8
|
#
|
|
9
|
-
# The
|
|
9
|
+
# The seven checks:
|
|
10
10
|
#
|
|
11
11
|
# class_validity - every declared class exists in the compiled Tailwind output;
|
|
12
12
|
# catches typos, hallucinated classes, and theme values that
|
|
13
13
|
# don't exist (requires known_classes:)
|
|
14
14
|
# self_conflicts - no declaration conflicts with itself; catches e.g.
|
|
15
15
|
# css "block flex" silently dropping "block"
|
|
16
|
+
# cross_declaration_conflicts - no class declared in one place is silently
|
|
17
|
+
# dropped when a different declaration merges on top of it;
|
|
18
|
+
# catches e.g. a base leading-snug that a size axis's text-sm
|
|
19
|
+
# overrides (font-size utilities also set line-height)
|
|
16
20
|
# method_rules - every Symbol in css/data/aria/attribute rules resolves to a
|
|
17
21
|
# method; catches render-time NoMethodErrors
|
|
18
22
|
# axes_settable - every axis has an initialize param or @ivar assignment;
|
|
@@ -59,6 +63,7 @@ class ViewComponentCssDsl::Verifier
|
|
|
59
63
|
def verify(component)
|
|
60
64
|
check_class_validity(component) +
|
|
61
65
|
check_self_conflicts(component) +
|
|
66
|
+
check_cross_declaration_conflicts(component) +
|
|
62
67
|
check_method_rules(component) +
|
|
63
68
|
check_axes_settable(component) +
|
|
64
69
|
check_variant_matrix(component) +
|
|
@@ -106,6 +111,32 @@ class ViewComponentCssDsl::Verifier
|
|
|
106
111
|
end
|
|
107
112
|
end
|
|
108
113
|
|
|
114
|
+
# A class declared in one place can be silently dropped when a *different*
|
|
115
|
+
# declaration is merged on top of it — the blind spot check_self_conflicts
|
|
116
|
+
# (one declaration at a time) and check_variant_matrix (exceptions only) both
|
|
117
|
+
# miss. The footgun: a base `leading-snug` that a size axis's `text-sm`
|
|
118
|
+
# overrides, because Tailwind font-size utilities also set line-height.
|
|
119
|
+
#
|
|
120
|
+
# Reported as warnings, not errors: a cross-declaration drop is often
|
|
121
|
+
# intentional — a variant overriding a base default. We suppress same-family
|
|
122
|
+
# overrides (p-2 -> p-8) and surface only drops whose winning class is a
|
|
123
|
+
# different utility family (leading-snug dropped by text-sm), which is almost
|
|
124
|
+
# always a surprise.
|
|
125
|
+
def check_cross_declaration_conflicts(component)
|
|
126
|
+
axis_combinations(component).flat_map do |combo|
|
|
127
|
+
tokens = contributing_tokens(component, combo)
|
|
128
|
+
next [] if tokens.size < 2
|
|
129
|
+
|
|
130
|
+
survivors = component.smart_merge(tokens.map { |t| t[:class] }.join(" ")).split
|
|
131
|
+
dropped_conflicts(component, tokens, survivors).map do |dropped, winner|
|
|
132
|
+
finding(component, :cross_declaration_conflicts, :warning,
|
|
133
|
+
"#{dropped[:label]}: \"#{dropped[:class]}\" is silently dropped when " \
|
|
134
|
+
"merged with \"#{winner[:class]}\" from #{winner[:label]} (both set " \
|
|
135
|
+
"the same CSS property)")
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
109
140
|
# Symbols in css/data/aria/attribute rules are method references; each must
|
|
110
141
|
# resolve on the component. The DSL only raises for these at render time.
|
|
111
142
|
def check_method_rules(component)
|
|
@@ -227,6 +258,65 @@ class ViewComponentCssDsl::Verifier
|
|
|
227
258
|
component.private_method_defined?(method_name)
|
|
228
259
|
end
|
|
229
260
|
|
|
261
|
+
##################################################################################
|
|
262
|
+
# Cross-declaration conflicts
|
|
263
|
+
##################################################################################
|
|
264
|
+
|
|
265
|
+
# {class:, label:} for every class the static declarations contribute for this
|
|
266
|
+
# combo, in merge order: base declarations first, then matching axis rules.
|
|
267
|
+
def contributing_tokens(component, combo)
|
|
268
|
+
declarations =
|
|
269
|
+
base_declarations(component) + matching_axis_declarations(component, combo)
|
|
270
|
+
declarations.flat_map do |label, styles|
|
|
271
|
+
styles.split.map { |cls| {class: cls, label:} }
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def base_declarations(component)
|
|
276
|
+
component._css_base_declarations.map { |styles| ["css \"#{styles}\"", styles] }
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def matching_axis_declarations(component, combo)
|
|
280
|
+
component._css_axis_rules.filter_map do |rule|
|
|
281
|
+
matches = rule[:axes].all? { |axis, value| combo[axis] == value }
|
|
282
|
+
[axis_label(rule[:axes]), rule[:styles]] if matches
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# [dropped_token, winning_token] pairs the merge silently removed. A class is
|
|
287
|
+
# dropped when it's absent from survivors; its winner is the later class that
|
|
288
|
+
# displaced it (found by pairwise re-merge). Only cross-declaration,
|
|
289
|
+
# cross-family pairs are returned — same-declaration drops belong to
|
|
290
|
+
# check_self_conflicts, same-family drops are intentional overrides.
|
|
291
|
+
def dropped_conflicts(component, tokens, survivors)
|
|
292
|
+
tokens.each_with_index.filter_map do |token, index|
|
|
293
|
+
next if survivors.include?(token[:class])
|
|
294
|
+
|
|
295
|
+
winner = winning_token(component, tokens, index)
|
|
296
|
+
next unless winner
|
|
297
|
+
next if winner[:label] == token[:label]
|
|
298
|
+
next if utility_family(winner[:class]) == utility_family(token[:class])
|
|
299
|
+
|
|
300
|
+
[token, winner]
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# The later token that displaces tokens[index]: the first subsequent token
|
|
305
|
+
# that, merged after it, wins outright.
|
|
306
|
+
def winning_token(component, tokens, index)
|
|
307
|
+
dropped = tokens[index][:class]
|
|
308
|
+
tokens.drop(index + 1).find do |candidate|
|
|
309
|
+
merged = component.smart_merge("#{dropped} #{candidate[:class]}").split
|
|
310
|
+
merged == [candidate[:class]]
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# The utility family of a Tailwind class, ignoring variant prefixes and
|
|
315
|
+
# negativity: hover:-mt-2 -> "mt", leading-snug -> "leading", text-sm -> "text".
|
|
316
|
+
def utility_family(token)
|
|
317
|
+
token.split(":").last.delete_prefix("-").split("-").first
|
|
318
|
+
end
|
|
319
|
+
|
|
230
320
|
##################################################################################
|
|
231
321
|
# Axis settability
|
|
232
322
|
##################################################################################
|