clsx-rails 3.0.0 → 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: 2da83a9921c3d24cf5c9743bcaf1a11a63bae43f38f7ba197116436f8b2471d5
4
- data.tar.gz: 280c9dfc3fc501ce352d755d620f49ec4e58b8c6de7e976487cd80ccf358e700
3
+ metadata.gz: bef11730f6fbfe63e305c35697e86117393c6114915d7b68853b3b2e12ee6737
4
+ data.tar.gz: d8244bf7d2fe9eaf563948b26eafbed1a4bd76aa17328aacd97ba2d513186a41
5
5
  SHA512:
6
- metadata.gz: 33c8950cb1a10e94b22fc4a442dba3181dab708f7d1d6abec2bbd0b42aae009832aebcbd1c963c6050fa0ba397f4e4f960743675679d7ad46c507a3b2d9efb52
7
- data.tar.gz: 47bb02df172c9c3d98251234112eaaef97b150d39b49c1e51a1fc5caf6d41c310719af75de32be86a15e7cb3a6062dd67fdbbe90e2c9765d9f3907f51399737f
6
+ metadata.gz: 7d99571d5e502310350a03aa3d2fc0da8f815f1bb8f33fa9bfd0e15da17439a26ab078d185c0c9fc7943ede6190dece4067d5d1996da71420fa9c1ee08438891
7
+ data.tar.gz: 38797c2478f8fe3da811851d79c00a3045aa629edaa4dd2b7708fd944793fc34b0a8e84ff342247ef1cba10befd1029777b42874673f4eeaf85ea11c2d95c0e7
data/CHANGELOG.md CHANGED
@@ -6,6 +6,20 @@ 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
+
17
+ ## v3.0.1 (2026-02-27)
18
+
19
+ ### Changed
20
+ - Require clsx-ruby >= 1.1.3 for correct deduplication behavior
21
+ - Updated benchmark numbers to reflect current performance (2-4x faster)
22
+
9
23
  ## v3.0.0 (2026-02-13)
10
24
 
11
25
  ### Added
data/README.md CHANGED
@@ -1,8 +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
- > Rails view helper for constructing CSS class strings conditionally. Powered by [clsx-ruby](https://github.com/svyatov/clsx-ruby).
4
-
5
- Automatically adds `clsx` and `cn` helpers to all Rails views. The fastest alternative to Rails `class_names`.
3
+ > The fastest conditional CSS class builder for Rails 2–4x faster drop-in replacement for `class_names`.
4
+ > Powered by [clsx-ruby](https://github.com/svyatov/clsx-ruby).
5
+
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)
6
27
 
7
28
  ## Quick Start
8
29
 
@@ -13,7 +34,7 @@ bundle add clsx-rails
13
34
  Or add it manually to the Gemfile:
14
35
 
15
36
  ```ruby
16
- gem 'clsx-rails', '~> 3.0'
37
+ gem 'clsx-rails', '~> 3.1'
17
38
  ```
18
39
 
19
40
  That's it — `clsx` and `cn` are now available in all your views:
@@ -24,36 +45,41 @@ That's it — `clsx` and `cn` are now available in all your views:
24
45
  <% end %>
25
46
  ```
26
47
 
27
- ## Why clsx-rails over Rails `class_names`?
48
+ ## Why clsx-rails?
28
49
 
29
- ### Faster
50
+ ### Blazing fast
30
51
 
31
- **3-8x faster** than Rails `class_names` across every scenario:
52
+ **2–4x faster** than Rails `class_names` never slower, on realistic markup:
32
53
 
33
54
  | Scenario | clsx | Rails `class_names` | Speedup |
34
55
  |---|---|---|---|
35
- | Single string | 7.6M i/s | 911K i/s | **8.5x** |
36
- | String + hash | 2.4M i/s | 580K i/s | **4.1x** |
37
- | String array | 1.4M i/s | 357K i/s | **4.0x** |
38
- | Multiple strings | 1.5M i/s | 414K i/s | **3.7x** |
39
- | Hash | 2.2M i/s | 670K i/s | **3.3x** |
40
- | Mixed types | 852K i/s | 367K i/s | **2.3x** |
56
+ | String array | 1.2M i/s | 317K i/s | **3.9x** |
57
+ | Multiple strings | 1.3M i/s | 346K i/s | **3.8x** |
58
+ | Single string | 2.3M i/s | 812K i/s | **2.9x** |
59
+ | Mixed types | 901K i/s | 331K i/s | **2.7x** |
60
+ | Hash | 1.7M i/s | 684K i/s | **2.4x** |
61
+ | String + hash | 1.2M i/s | 550K i/s | **2.1x** |
41
62
 
42
63
  <sup>Ruby 4.0.1, Apple M1 Pro. Reproduce: `bundle exec ruby benchmark/run.rb`</sup>
43
64
 
44
- ### More features
65
+ ### More feature-rich than `class_names`
45
66
 
46
67
  | Feature | clsx-rails | Rails `class_names` |
47
68
  |---|---|---|
48
- | Conditional classes | yes | yes |
49
- | Auto-deduplication | yes | yes |
50
- | 3-8x faster | yes | no |
51
- | Returns `nil` when empty | yes | no (returns `""`) |
52
- | Complex hash keys | yes | no |
53
- | 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 | ✅ | ❌ |
54
76
 
55
77
  ## Usage
56
78
 
79
+ `clsx` and its `cn` alias are available in every view — no `include`, no setup.
80
+
81
+ ### Input types
82
+
57
83
  ```ruby
58
84
  # Strings (variadic)
59
85
  clsx('foo', true && 'bar', 'baz')
@@ -75,14 +101,31 @@ cn(['foo', nil, false, 'bar'])
75
101
  clsx(['foo'], ['', nil, false, 'bar'], [['baz', [['hello'], 'there']]])
76
102
  # => 'foo bar baz hello there'
77
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
+
78
120
  # Kitchen sink (with nesting)
79
121
  cn('foo', ['bar', { baz: false, bat: nil }, ['hello', ['world']]], 'cya')
80
122
  # => 'foo bar hello world cya'
81
123
  ```
82
124
 
83
- ### ERB
125
+ ### Template engines
84
126
 
85
127
  ```erb
128
+ <%# ERB %>
86
129
  <%= tag.div class: clsx('foo', 'baz', 'is-active': @active) do %>
87
130
  Hello, world!
88
131
  <% end %>
@@ -92,68 +135,137 @@ cn('foo', ['bar', { baz: false, bat: nil }, ['hello', ['world']]], 'cya')
92
135
  </div>
93
136
  ```
94
137
 
95
- ### HAML
96
-
97
138
  ```haml
139
+ -# HAML
98
140
  %div{class: clsx('foo', 'baz', 'is-active': @active)}
99
141
  Hello, world!
100
142
  ```
101
143
 
102
- ### Slim
103
-
104
144
  ```slim
145
+ / Slim
105
146
  div class=clsx('foo', 'baz', 'is-active': @active)
106
147
  | Hello, world!
107
148
  ```
108
149
 
109
- ## Framework Examples
150
+ ## Framework Integration
110
151
 
111
- ### 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:
112
158
 
113
159
  ```ruby
114
- class AlertComponent < ViewComponent::Base
115
- def initialize(variant: :info, dismissible: false)
116
- @variant = variant
117
- @dismissible = dismissible
160
+ class NavLink < ViewComponent::Base
161
+ include Clsx::Helper
162
+
163
+ def initialize(active: false)
164
+ @active = active
118
165
  end
119
166
 
120
167
  def classes
121
- 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
+ )
122
173
  end
123
174
  end
124
175
  ```
125
176
 
126
- ### 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:
127
210
 
128
211
  ```ruby
129
- 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
130
225
  include Clsx::Helper
226
+ end
227
+ ```
131
228
 
132
- def initialize(color: :blue, pill: false)
133
- @color = color
134
- @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
135
238
  end
136
239
 
137
- def view_template
138
- span(class: clsx("badge", "badge-#{@color}", pill: @pill)) { yield }
240
+ def classes
241
+ clsx("alert", "alert-#{@variant}", @html_class, dismissible: @dismissible)
139
242
  end
140
243
  end
141
244
  ```
142
245
 
143
- ### 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:
144
254
 
145
255
  ```ruby
146
- class NavLink < ViewComponent::Base
147
- def initialize(active: false)
148
- @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
149
265
  end
150
266
 
151
- def classes
152
- clsx(
153
- 'px-3 py-2 rounded-md text-sm font-medium transition-colors',
154
- 'text-white bg-indigo-600': @active,
155
- 'text-gray-300 hover:text-white hover:bg-gray-700': !@active
156
- )
267
+ def view_template
268
+ span(class: clsx("badge", "badge-#{@color}", @attributes[:class], pill: @pill)) { yield }
157
269
  end
158
270
  end
159
271
  ```
@@ -165,30 +277,27 @@ end
165
277
  clsx(nil, false) # => nil
166
278
  ```
167
279
 
168
- 2. **Deduplication** — duplicate classes are automatically removed:
280
+ 2. **Deduplication** — Duplicate classes are automatically removed, even across multi-token strings:
169
281
  ```ruby
170
- clsx('foo', 'foo') # => 'foo'
282
+ clsx('foo', 'foo') # => 'foo'
283
+ clsx('foo bar', 'foo') # => 'foo bar'
171
284
  ```
172
285
 
173
- 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:
174
287
  ```ruby
175
288
  clsx('foo' => 0, bar: []) # => 'foo bar'
176
289
  ```
177
290
 
178
- 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:
179
292
  ```ruby
180
293
  clsx([{ foo: true }, 'bar'] => true) # => 'foo bar'
181
294
  ```
182
295
 
183
- 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:
184
297
  ```ruby
185
298
  clsx('', proc {}, -> {}, nil, false, true) # => nil
186
299
  ```
187
300
 
188
- ## Looking for a framework-agnostic version?
189
-
190
- See [clsx-ruby](https://github.com/svyatov/clsx-ruby) — works with Rails, Sinatra, Hanami, or plain Ruby.
191
-
192
301
  ## Supported Versions
193
302
 
194
303
  Ruby 3.2+ and Rails 7.2+.
@@ -196,9 +305,9 @@ Ruby 3.2+ and Rails 7.2+.
196
305
  ## Development
197
306
 
198
307
  ```bash
199
- bin/setup # install dependencies
200
- bundle exec rake test # run tests
201
- bundle exec ruby benchmark/run.rb # run benchmarks
308
+ bin/setup # install dependencies
309
+ bundle exec rake test # run tests
310
+ bundle exec ruby benchmark/run.rb # run benchmarks
202
311
  ```
203
312
 
204
313
  ## Contributing
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Clsx
4
4
  module Rails
5
- VERSION = '3.0.0'
5
+ VERSION = '3.1.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: clsx-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leonid Svyatov
8
- bindir: exe
8
+ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
@@ -29,16 +29,17 @@ dependencies:
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: '1.1'
32
+ version: '1.2'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '1.1'
40
- description: Adds clsx and cn helpers to all Rails views for constructing CSS class
41
- strings conditionally
39
+ version: '1.2'
40
+ description: Build CSS class strings from conditional expressions, hashes, arrays,
41
+ or nested structures. 2-4x faster drop-in replacement for Rails class_names. Supports
42
+ ViewComponent, Phlex, and Tailwind CSS.
42
43
  email:
43
44
  - leonid@svyatov.com
44
45
  executables: []
@@ -54,7 +55,6 @@ homepage: https://github.com/svyatov/clsx-rails
54
55
  licenses:
55
56
  - MIT
56
57
  metadata:
57
- homepage_uri: https://github.com/svyatov/clsx-rails
58
58
  source_code_uri: https://github.com/svyatov/clsx-rails
59
59
  changelog_uri: https://github.com/svyatov/clsx-rails/blob/main/CHANGELOG.md
60
60
  rubygems_mfa_required: 'true'
@@ -72,7 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
72
72
  - !ruby/object:Gem::Version
73
73
  version: '0'
74
74
  requirements: []
75
- rubygems_version: 4.0.6
75
+ rubygems_version: 4.0.12
76
76
  specification_version: 4
77
- summary: Rails view helper integration for clsx-ruby
77
+ summary: The fastest conditional CSS class builder for Rails
78
78
  test_files: []