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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 494769078a576e9a1f7d3be9b2819ed2c93bf148da58154c68356c01410d9e8f
4
- data.tar.gz: 529f35969651591c73f2c21012ab828c84305f94df5317f612fc4dc13b75cc39
3
+ metadata.gz: 8881683c0d397b134f9fc656eda692df513d6bd3160c23ef397a212f51cd5eee
4
+ data.tar.gz: 58f07649276631f545c464dc758df825252ef1aeecd03524266528a886a3d9ba
5
5
  SHA512:
6
- metadata.gz: 365ce4701b070176cfda8d7a7dcb8005011326d3b888315ce0d37ba80cd2026bc8b5e3dcf8833747911e1ad5e5ca4d752ec28cf6dfa6b058240b847950bd1ca3
7
- data.tar.gz: fc2ff970cc016c1fe8c788c622af0894c8d0c701afa9861fc40bc34c7926c3d2c19912c3d51287cf43da6d68c826fb4f4163e75ccb1714b874fb7c0621ff48ac
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`). Six checks run:
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 six checks:
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
  ##################################################################################
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ViewComponentCssDsl
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.5"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: view_component_css_dsl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeff Lange