clsx-rails 3.0.1 → 3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 62267a91d9e4596cd5a4da9cf8569a0ac6420e6eb4368a099b64ed3a07e7bc18
4
- data.tar.gz: a0d7c426b1aeea779d70a5a10cc800f9de7c5376cc3fcf5c27ae409c8ee07097
3
+ metadata.gz: bef11730f6fbfe63e305c35697e86117393c6114915d7b68853b3b2e12ee6737
4
+ data.tar.gz: d8244bf7d2fe9eaf563948b26eafbed1a4bd76aa17328aacd97ba2d513186a41
5
5
  SHA512:
6
- metadata.gz: a0f10f1585daeeae9872a3e07faff6e427c453709dce9e2d698f9dca89073068eb0080fcf28c6a2d4d023c2fa68ecaf5810cdcaa12bc0728c868f88cb8c34fda
7
- data.tar.gz: e3a77cfc660fe7295027d7447394c0b37602ba077ab6ea77c5d19cafdaa90109d1dd69d1128f3f8e504703fd996a3ced922d8185a1a4669656360df647de7e7f
6
+ metadata.gz: 7d99571d5e502310350a03aa3d2fc0da8f815f1bb8f33fa9bfd0e15da17439a26ab078d185c0c9fc7943ede6190dece4067d5d1996da71420fa9c1ee08438891
7
+ data.tar.gz: 38797c2478f8fe3da811851d79c00a3045aa629edaa4dd2b7708fd944793fc34b0a8e84ff342247ef1cba10befd1029777b42874673f4eeaf85ea11c2d95c0e7
data/CHANGELOG.md CHANGED
@@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## v3.1.0 (2026-07-01)
10
+
11
+ ### Added
12
+ - `twm` Tailwind class-merge helper, available in all views via clsx-ruby 1.2.0's optional `tailwind_merge` integration (opt-in: `require 'clsx/tailwind_merge'` + `Clsx.merger =`)
13
+
14
+ ### Changed
15
+ - Require clsx-ruby `~> 1.2` (was `~> 1.1, >= 1.1.3`); picks up faster single-string and hash paths
16
+
9
17
  ## v3.0.1 (2026-02-27)
10
18
 
11
19
  ### Changed
data/README.md CHANGED
@@ -1,9 +1,29 @@
1
1
  # clsx-rails [![Gem Version](https://img.shields.io/gem/v/clsx-rails)](https://rubygems.org/gems/clsx-rails) [![Codecov](https://img.shields.io/codecov/c/github/svyatov/clsx-rails)](https://app.codecov.io/gh/svyatov/clsx-rails) [![CI](https://github.com/svyatov/clsx-rails/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/svyatov/clsx-rails/actions?query=workflow%3ACI) [![GitHub License](https://img.shields.io/github/license/svyatov/clsx-rails)](LICENSE.txt)
2
2
 
3
- > The fastest conditional CSS class builder for Rails — 2-4x faster drop-in replacement for `class_names`.
3
+ > The fastest conditional CSS class builder for Rails — 24x faster drop-in replacement for `class_names`.
4
4
  > Powered by [clsx-ruby](https://github.com/svyatov/clsx-ruby).
5
5
 
6
- Adds `clsx` and `cn` helpers to all views automatically.
6
+ Auto-loads `clsx` and `cn` helpers into every Rails view. Zero configuration.
7
+
8
+ ## Contents
9
+
10
+ - [Quick Start](#quick-start)
11
+ - [Why clsx-rails?](#why-clsx-rails)
12
+ - [Blazing fast](#blazing-fast)
13
+ - [More feature-rich than `class_names`](#more-feature-rich-than-class_names)
14
+ - [Usage](#usage)
15
+ - [Input types](#input-types)
16
+ - [Template engines](#template-engines)
17
+ - [Framework Integration](#framework-integration)
18
+ - [Tailwind CSS](#tailwind-css)
19
+ - [Merging conflicting utilities](#merging-conflicting-utilities)
20
+ - [ViewComponent](#viewcomponent)
21
+ - [Phlex](#phlex)
22
+ - [Differences from JavaScript clsx](#differences-from-javascript-clsx)
23
+ - [Supported Versions](#supported-versions)
24
+ - [Development](#development)
25
+ - [Contributing](#contributing)
26
+ - [License](#license)
7
27
 
8
28
  ## Quick Start
9
29
 
@@ -14,7 +34,7 @@ bundle add clsx-rails
14
34
  Or add it manually to the Gemfile:
15
35
 
16
36
  ```ruby
17
- gem 'clsx-rails', '~> 3.0'
37
+ gem 'clsx-rails', '~> 3.1'
18
38
  ```
19
39
 
20
40
  That's it — `clsx` and `cn` are now available in all your views:
@@ -25,11 +45,11 @@ That's it — `clsx` and `cn` are now available in all your views:
25
45
  <% end %>
26
46
  ```
27
47
 
28
- ## Why clsx-rails over Rails `class_names`?
48
+ ## Why clsx-rails?
29
49
 
30
- ### Faster
50
+ ### Blazing fast
31
51
 
32
- **2-4x faster** than Rails `class_names` across every scenario:
52
+ **24x faster** than Rails `class_names` never slower, on realistic markup:
33
53
 
34
54
  | Scenario | clsx | Rails `class_names` | Speedup |
35
55
  |---|---|---|---|
@@ -42,19 +62,24 @@ That's it — `clsx` and `cn` are now available in all your views:
42
62
 
43
63
  <sup>Ruby 4.0.1, Apple M1 Pro. Reproduce: `bundle exec ruby benchmark/run.rb`</sup>
44
64
 
45
- ### More features
65
+ ### More feature-rich than `class_names`
46
66
 
47
67
  | Feature | clsx-rails | Rails `class_names` |
48
68
  |---|---|---|
49
- | Conditional classes | yes | yes |
50
- | Auto-deduplication | yes | yes |
51
- | 2-4x faster | yes | no |
52
- | Returns `nil` when empty | yes | no (returns `""`) |
53
- | Complex hash keys | yes | no |
54
- | Short `cn` alias | yes | no |
69
+ | Conditional classes | | |
70
+ | Auto-deduplication | | |
71
+ | 2–4× faster | | |
72
+ | Returns `nil` when empty | | (returns `""`) |
73
+ | Complex hash keys | | |
74
+ | Tailwind conflict merge (`twm`) | | |
75
+ | Short `cn` alias | ✅ | ❌ |
55
76
 
56
77
  ## Usage
57
78
 
79
+ `clsx` and its `cn` alias are available in every view — no `include`, no setup.
80
+
81
+ ### Input types
82
+
58
83
  ```ruby
59
84
  # Strings (variadic)
60
85
  clsx('foo', true && 'bar', 'baz')
@@ -76,14 +101,31 @@ cn(['foo', nil, false, 'bar'])
76
101
  clsx(['foo'], ['', nil, false, 'bar'], [['baz', [['hello'], 'there']]])
77
102
  # => 'foo bar baz hello there'
78
103
 
104
+ # Symbols
105
+ clsx(:foo, :'bar-baz')
106
+ # => 'foo bar-baz'
107
+
108
+ # Numbers
109
+ clsx(1, 2, 3)
110
+ # => '1 2 3'
111
+
112
+ # Multi-token strings (deduplicated)
113
+ clsx('a b', 'b c')
114
+ # => 'a b c'
115
+
116
+ # Whitespace is normalized; blank/whitespace-only => nil
117
+ clsx(" a\tb\n\nc ")
118
+ # => 'a b c'
119
+
79
120
  # Kitchen sink (with nesting)
80
121
  cn('foo', ['bar', { baz: false, bat: nil }, ['hello', ['world']]], 'cya')
81
122
  # => 'foo bar hello world cya'
82
123
  ```
83
124
 
84
- ### ERB
125
+ ### Template engines
85
126
 
86
127
  ```erb
128
+ <%# ERB %>
87
129
  <%= tag.div class: clsx('foo', 'baz', 'is-active': @active) do %>
88
130
  Hello, world!
89
131
  <% end %>
@@ -93,68 +135,137 @@ cn('foo', ['bar', { baz: false, bat: nil }, ['hello', ['world']]], 'cya')
93
135
  </div>
94
136
  ```
95
137
 
96
- ### HAML
97
-
98
138
  ```haml
139
+ -# HAML
99
140
  %div{class: clsx('foo', 'baz', 'is-active': @active)}
100
141
  Hello, world!
101
142
  ```
102
143
 
103
- ### Slim
104
-
105
144
  ```slim
145
+ / Slim
106
146
  div class=clsx('foo', 'baz', 'is-active': @active)
107
147
  | Hello, world!
108
148
  ```
109
149
 
110
- ## Framework Examples
150
+ ## Framework Integration
111
151
 
112
- ### ViewComponent
152
+ Plain Rails views (ERB, HAML, Slim) get `clsx`/`cn` automatically. ViewComponent and Phlex
153
+ render in their own object hierarchies, so include `Clsx::Helper` once in your base class.
154
+
155
+ ### Tailwind CSS
156
+
157
+ Compose conditional utilities without string juggling:
113
158
 
114
159
  ```ruby
115
- class AlertComponent < ViewComponent::Base
116
- def initialize(variant: :info, dismissible: false)
117
- @variant = variant
118
- @dismissible = dismissible
160
+ class NavLink < ViewComponent::Base
161
+ include Clsx::Helper
162
+
163
+ def initialize(active: false)
164
+ @active = active
119
165
  end
120
166
 
121
167
  def classes
122
- clsx("alert", "alert-#{@variant}", dismissible: @dismissible)
168
+ clsx(
169
+ 'px-3 py-2 rounded-md text-sm font-medium transition-colors',
170
+ 'text-white bg-indigo-600': @active,
171
+ 'text-gray-300 hover:text-white hover:bg-gray-700': !@active
172
+ )
123
173
  end
124
174
  end
125
175
  ```
126
176
 
127
- ### Phlex
177
+ #### Merging conflicting utilities
178
+
179
+ `clsx`/`cn` keep every class, so conflicting Tailwind utilities both survive:
180
+
181
+ ```ruby
182
+ clsx('px-2 px-4') # => 'px-2 px-4'
183
+ ```
184
+
185
+ For conflict resolution, opt into the [`tailwind_merge`](https://github.com/gjtorikian/tailwind_merge)
186
+ gem. Add it to your `Gemfile` (clsx-rails itself pulls in only clsx-ruby), then require the
187
+ integration once at boot:
188
+
189
+ ```ruby
190
+ # Gemfile
191
+ gem 'tailwind_merge'
192
+
193
+ # config/initializers/clsx.rb
194
+ require 'clsx/tailwind_merge'
195
+
196
+ # Optional: configure the merger (prefix, cache size, custom theme, …)
197
+ Clsx.merger = TailwindMerge::Merger.new
198
+ ```
199
+
200
+ This adds a merged variant — `twm` — available in every view alongside `clsx`/`cn`.
201
+ `clsx`/`cn` stay pure; only `twm` merges, and the last conflicting utility wins:
202
+
203
+ ```ruby
204
+ twm('px-2 px-4') # => 'px-4'
205
+ twm('p-4', 'p-2', 'bg-red', 'bg-blue') # => 'p-2 bg-blue'
206
+ clsx('px-2 px-4') # => 'px-2 px-4' (unchanged)
207
+ ```
208
+
209
+ It accepts the same arguments as `clsx` (strings, hashes, arrays, nesting), then merges:
128
210
 
129
211
  ```ruby
130
- class Badge < Phlex::HTML
212
+ twm(['px-2', 'rounded'], 'px-4', 'px-6': active) # => 'rounded px-6' (when active)
213
+
214
+ # Also available as a bracket API and a module method:
215
+ Twm['px-2 px-4'] # => 'px-4'
216
+ Clsx.twm('px-2 px-4') # => 'px-4'
217
+ ```
218
+
219
+ ### ViewComponent
220
+
221
+ Include the mixin once in your base component instead of per component:
222
+
223
+ ```ruby
224
+ class ApplicationComponent < ViewComponent::Base
131
225
  include Clsx::Helper
226
+ end
227
+ ```
132
228
 
133
- def initialize(color: :blue, pill: false)
134
- @color = color
135
- @pill = pill
229
+ Accept a caller-supplied `class:` and merge it — clsx dedupes across every argument, so
230
+ callers can extend or repeat classes safely:
231
+
232
+ ```ruby
233
+ class AlertComponent < ApplicationComponent
234
+ def initialize(variant: :info, dismissible: false, class: nil)
235
+ @variant = variant
236
+ @dismissible = dismissible
237
+ @html_class = binding.local_variable_get(:class) # `class` is a Ruby keyword
136
238
  end
137
239
 
138
- def view_template
139
- span(class: clsx("badge", "badge-#{@color}", pill: @pill)) { yield }
240
+ def classes
241
+ clsx("alert", "alert-#{@variant}", @html_class, dismissible: @dismissible)
140
242
  end
141
243
  end
142
244
  ```
143
245
 
144
- ### Tailwind CSS
246
+ ```erb
247
+ <div class="<%= classes %>">...</div>
248
+ ```
249
+
250
+ ### Phlex
251
+
252
+ Include the mixin once in your base component, then merge caller-supplied attributes —
253
+ clsx dedupes across every argument:
145
254
 
146
255
  ```ruby
147
- class NavLink < ViewComponent::Base
148
- def initialize(active: false)
149
- @active = active
256
+ class ApplicationComponent < Phlex::HTML
257
+ include Clsx::Helper
258
+ end
259
+
260
+ class Badge < ApplicationComponent
261
+ def initialize(color: :blue, pill: false, **attributes)
262
+ @color = color
263
+ @pill = pill
264
+ @attributes = attributes
150
265
  end
151
266
 
152
- def classes
153
- clsx(
154
- 'px-3 py-2 rounded-md text-sm font-medium transition-colors',
155
- 'text-white bg-indigo-600': @active,
156
- 'text-gray-300 hover:text-white hover:bg-gray-700': !@active
157
- )
267
+ def view_template
268
+ span(class: clsx("badge", "badge-#{@color}", @attributes[:class], pill: @pill)) { yield }
158
269
  end
159
270
  end
160
271
  ```
@@ -166,30 +277,27 @@ end
166
277
  clsx(nil, false) # => nil
167
278
  ```
168
279
 
169
- 2. **Deduplication** — duplicate classes are automatically removed:
280
+ 2. **Deduplication** — Duplicate classes are automatically removed, even across multi-token strings:
170
281
  ```ruby
171
- clsx('foo', 'foo') # => 'foo'
282
+ clsx('foo', 'foo') # => 'foo'
283
+ clsx('foo bar', 'foo') # => 'foo bar'
172
284
  ```
173
285
 
174
- 3. **Falsy values** — in Ruby only `false` and `nil` are falsy, so `0`, `''`, `[]`, `{}` are all truthy:
286
+ 3. **Falsy values** — In Ruby only `false` and `nil` are falsy, so `0`, `''`, `[]`, `{}` are all truthy:
175
287
  ```ruby
176
288
  clsx('foo' => 0, bar: []) # => 'foo bar'
177
289
  ```
178
290
 
179
- 4. **Complex hash keys** — any valid `clsx` input works as a hash key:
291
+ 4. **Complex hash keys** — Any valid `clsx` input works as a hash key:
180
292
  ```ruby
181
293
  clsx([{ foo: true }, 'bar'] => true) # => 'foo bar'
182
294
  ```
183
295
 
184
- 5. **Ignored values** — boolean `true` and `Proc`/lambda objects are silently ignored:
296
+ 5. **Ignored values** — Boolean `true` and `Proc`/lambda objects are silently ignored:
185
297
  ```ruby
186
298
  clsx('', proc {}, -> {}, nil, false, true) # => nil
187
299
  ```
188
300
 
189
- ## Looking for a framework-agnostic version?
190
-
191
- See [clsx-ruby](https://github.com/svyatov/clsx-ruby) — works with Rails, Sinatra, Hanami, or plain Ruby.
192
-
193
301
  ## Supported Versions
194
302
 
195
303
  Ruby 3.2+ and Rails 7.2+.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Clsx
4
4
  module Rails
5
- VERSION = '3.0.1'
5
+ VERSION = '3.1.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: clsx-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.1
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leonid Svyatov
@@ -29,20 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: '1.1'
33
- - - ">="
34
- - !ruby/object:Gem::Version
35
- version: 1.1.3
32
+ version: '1.2'
36
33
  type: :runtime
37
34
  prerelease: false
38
35
  version_requirements: !ruby/object:Gem::Requirement
39
36
  requirements:
40
37
  - - "~>"
41
38
  - !ruby/object:Gem::Version
42
- version: '1.1'
43
- - - ">="
44
- - !ruby/object:Gem::Version
45
- version: 1.1.3
39
+ version: '1.2'
46
40
  description: Build CSS class strings from conditional expressions, hashes, arrays,
47
41
  or nested structures. 2-4x faster drop-in replacement for Rails class_names. Supports
48
42
  ViewComponent, Phlex, and Tailwind CSS.
@@ -78,7 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
78
72
  - !ruby/object:Gem::Version
79
73
  version: '0'
80
74
  requirements: []
81
- rubygems_version: 4.0.6
75
+ rubygems_version: 4.0.12
82
76
  specification_version: 4
83
77
  summary: The fastest conditional CSS class builder for Rails
84
78
  test_files: []