tailwind_theme 0.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +334 -0
- data/lib/tailwind_theme/version.rb +5 -0
- data/lib/tailwind_theme.rb +281 -0
- data/spec/fixtures/test-theme.yml +23 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/tailwind_theme/theme_spec.rb +92 -0
- data/spec/tailwind_theme_spec.rb +67 -0
- metadata +73 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e46f95b20a0a3dcbd2d84b08c6a5bb4c64a4647b9dbbb52062cb8b99d3cd9d97
|
4
|
+
data.tar.gz: d99001bb8cb8c02c4cec4379b4674dc1777ce90e9a08459cbb181edc541e9510
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 210f53e3e2a6bfd0be2f274876843824a74b4c76769dbbe207f5f9395c0c175a9a5266c98dcc2c8a5c5ff8326ad03b4e5e6e17aa3767261802818c28ce0261be
|
7
|
+
data.tar.gz: cf9f9d1bdf9d56ba98741ad908d2175c93f2f4376ba1d739c995b471fdcacc1ac347adadbac24b3053111327aac6064fe5282976e1428f4dd92c843018587fb5
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 James Fawks
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,334 @@
|
|
1
|
+
# TailwindTheme
|
2
|
+
|
3
|
+
Tailwind Theme makes it easy to DRY up your Tailwind CSS components by storing the css classnames in a single YAML file.
|
4
|
+
|
5
|
+
Tailwind Theme uses [`tailwind_merge`](https://github.com/gjtorikian/tailwind_merge) to merge the resulting
|
6
|
+
Tailwind CSS classes.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Install the gem and add to the application's Gemfile by executing:
|
11
|
+
|
12
|
+
$ bundle add tailwind_theme
|
13
|
+
|
14
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
15
|
+
|
16
|
+
$ gem install tailwind_theme
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
require "tailwind_theme"
|
20
|
+
|
21
|
+
theme = TailwindTheme.load_file("theme.yml")
|
22
|
+
theme.css("button", object: button)
|
23
|
+
|
24
|
+
```
|
25
|
+
|
26
|
+
Add your theme file location to your `tailwind.config.js` file.
|
27
|
+
|
28
|
+
```javascript
|
29
|
+
/** @type {import('tailwindcss').Config} */
|
30
|
+
module.exports = {
|
31
|
+
content: [
|
32
|
+
// Rest of content configuration
|
33
|
+
'path/to/theme/files/**/*.yml'
|
34
|
+
],
|
35
|
+
// Rest of tailwind configuration
|
36
|
+
}
|
37
|
+
```
|
38
|
+
|
39
|
+
## What is it for?
|
40
|
+
|
41
|
+
If you use Tailwind with your Rails application, you know Tailwind cannot process runtime configurations like
|
42
|
+
`"bg-#{color}"`, making it difficult to have dynamic yet predefined components.
|
43
|
+
|
44
|
+
So you are left with two options:
|
45
|
+
|
46
|
+
1. CSS Abstraction for components, like Buttons, Alerts, etc., which Tailwind **does not** recommend doing too early.
|
47
|
+
(See https://tailwindcss.com/docs/reusing-styles#compared-to-css-abstractions)
|
48
|
+
2. Or, create a partial for each Button variation (that is just a mess).
|
49
|
+
|
50
|
+
This is where `tailwind-theme` comes in by keeping your component themes consistent, dynamic, and in one place.
|
51
|
+
|
52
|
+
Rending a button dynamically is as simple as:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
@theme = TailwindTheme.load_file("theme.yml")
|
56
|
+
classnames = @theme.css("button", attributes: { variant: :outline, color: :blue, size: :base })
|
57
|
+
|
58
|
+
content_tag :button, content, class: classnames
|
59
|
+
```
|
60
|
+
|
61
|
+
Or if you have a button object or using View Components, you can do:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
class ButtonComponent < ViewComponent::Base
|
65
|
+
def initialize(theme:, size: :base, color: :blue, variant: :solid)
|
66
|
+
@theme = theme
|
67
|
+
@size = size
|
68
|
+
@color = color
|
69
|
+
@variant = variant
|
70
|
+
end
|
71
|
+
|
72
|
+
def call
|
73
|
+
content_tag :button, content, class: @theme.css("button", object: self)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
```
|
78
|
+
|
79
|
+
```erb
|
80
|
+
<%= render(ButtonComponent.new(variant: :outline, color: :blue)) %>
|
81
|
+
```
|
82
|
+
|
83
|
+
## Example Theme File
|
84
|
+
|
85
|
+
```yaml
|
86
|
+
text:
|
87
|
+
truncated: truncate whitespace-nowrap
|
88
|
+
button:
|
89
|
+
base: >-
|
90
|
+
text-center font-medium rounded-lg
|
91
|
+
focus:ring-4 focus:outline-none
|
92
|
+
pill: rounded-full
|
93
|
+
size:
|
94
|
+
xs: px-3 py-2 text-xs
|
95
|
+
sm: px-3 py-2 text-sm
|
96
|
+
base: px-5 py-2.5 text-sm
|
97
|
+
lg: px-5 py-3 text-base
|
98
|
+
xl: px-6 py-3.5 text-base
|
99
|
+
variant:
|
100
|
+
solid:
|
101
|
+
color:
|
102
|
+
blue: >-
|
103
|
+
text-white bg-blue-700 dark:bg-blue-600
|
104
|
+
hover:bg-blue-800 dark:hover:bg-blue-700
|
105
|
+
focus:ring-blue-300 dark:focus:ring-blue-800
|
106
|
+
red: >-
|
107
|
+
text-white bg-red-700 dark:bg-red-600
|
108
|
+
hover:bg-red-800 dark:hover:bg-red-700
|
109
|
+
focus:ring-red-300 dark:focus:ring-red-800
|
110
|
+
green: >-
|
111
|
+
text-white bg-green-700 dark:bg-green-600
|
112
|
+
hover:bg-green-800 dark:hover:bg-green-700
|
113
|
+
focus:ring-green-300 dark:focus:ring-green-800
|
114
|
+
yellow: >-
|
115
|
+
text-white bg-yellow-400 hover:bg-yellow-500
|
116
|
+
focus:ring-yellow-300 dark:focus:ring-yellow-900
|
117
|
+
indigo: >-
|
118
|
+
text-white bg-indigo-700 dark:bg-indigo-600
|
119
|
+
hover:bg-indigo-800 dark:hover:bg-indigo-700
|
120
|
+
focus:ring-indigo-300 dark:focus:ring-indigo-800
|
121
|
+
purple: >-
|
122
|
+
text-white bg-purple-700 dark:bg-purple-600
|
123
|
+
hover:bg-purple-800 dark:hover:bg-purple-700
|
124
|
+
focus:ring-purple-300 dark:focus:ring-purple-800
|
125
|
+
pink: >-
|
126
|
+
text-white bg-pink-700 dark:bg-pink-600
|
127
|
+
hover:bg-pink-800 dark:hover:bg-pink-700
|
128
|
+
focus:ring-pink-300 dark:focus:ring-pink-800
|
129
|
+
outline:
|
130
|
+
base: border
|
131
|
+
color:
|
132
|
+
blue: >-
|
133
|
+
text-blue-700 border-blue-700 dark:text-blue-500 dark:border-blue-500
|
134
|
+
hover:text-white hover:bg-blue-800 dark:hover:text-white dark:hover:bg-blue-500
|
135
|
+
focus:ring-blue-300 dark:focus:ring-blue-800
|
136
|
+
red: >-
|
137
|
+
text-red-700 border-red-700 dark:text-red-500 dark:border-red-500
|
138
|
+
hover:text-white hover:bg-red-800 dark:hover:text-white dark:hover:bg-red-500
|
139
|
+
focus:ring-red-300 dark:focus:ring-red-800
|
140
|
+
green: >-
|
141
|
+
text-green-700 border-green-700 dark:text-green-500 dark:border-green-500
|
142
|
+
hover:text-white hover:bg-green-800 dark:hover:text-white dark:hover:bg-green-500
|
143
|
+
focus:ring-green-300 dark:focus:ring-green-800
|
144
|
+
yellow: >-
|
145
|
+
text-yellow-700 border-yellow-700 dark:text-yellow-500 dark:border-yellow-500
|
146
|
+
hover:text-white hover:bg-yellow-800 dark:hover:text-white dark:hover:bg-yellow-500
|
147
|
+
focus:ring-yellow-300 dark:focus:ring-yellow-800
|
148
|
+
indigo: >-
|
149
|
+
text-indigo-700 border-indigo-700 dark:text-indigo-500 dark:border-indigo-500
|
150
|
+
hover:text-white hover:bg-indigo-800 dark:hover:text-white dark:hover:bg-indigo-500
|
151
|
+
focus:ring-indigo-300 dark:focus:ring-indigo-800
|
152
|
+
purple: >-
|
153
|
+
text-purple-700 border-purple-700 dark:text-purple-500 dark:border-purple-500
|
154
|
+
hover:text-white hover:bg-purple-800 dark:hover:text-white dark:hover:bg-purple-500
|
155
|
+
focus:ring-purple-300 dark:focus:ring-purple-800
|
156
|
+
pink: >-
|
157
|
+
text-pink-700 border-pink-700 dark:text-pink-500 dark:border-pink-500
|
158
|
+
hover:text-white hover:bg-pink-800 dark:hover:text-white dark:hover:bg-pink-500
|
159
|
+
focus:ring-pink-300 dark:focus:ring-pink-800
|
160
|
+
```
|
161
|
+
|
162
|
+
## Loading a Tailwind CSS theme
|
163
|
+
|
164
|
+
From a YAML file:
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
@theme = TailwindTheme.load_file("path/to/theme.yaml")
|
168
|
+
```
|
169
|
+
|
170
|
+
From a hash object (string keys):
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
@theme = TailwindTheme::Theme.new({
|
174
|
+
"button" => {
|
175
|
+
# Button Component
|
176
|
+
},
|
177
|
+
"alert" => {
|
178
|
+
# Alert Component
|
179
|
+
}
|
180
|
+
})
|
181
|
+
```
|
182
|
+
|
183
|
+
## Basic Usage
|
184
|
+
|
185
|
+
Retrieving a CSS class:
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
@theme.css("text.truncated")
|
189
|
+
```
|
190
|
+
|
191
|
+
Applying a theme to an object:
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
@theme.css(:button, object: button)
|
195
|
+
```
|
196
|
+
|
197
|
+
Applying a theme based on some attributes:
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
@theme.css(:button, attributes: { color: :blue })
|
201
|
+
```
|
202
|
+
|
203
|
+
Combining multiple themes from different paths:
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
@theme.merge_css([:modal, :dialog], attributes: modal_attributes)
|
207
|
+
```
|
208
|
+
|
209
|
+
## Methods
|
210
|
+
|
211
|
+
### `css(path, options=)`
|
212
|
+
|
213
|
+
Resolves the `path` and merges the Tailwind CSS classnames using `tailwind_merge`.
|
214
|
+
If `path` resolves to a [Hash], the methods and attributes from `options[:object]` and/or `options[:attributes]` will
|
215
|
+
be used to generate the classnames.
|
216
|
+
|
217
|
+
Arguments:
|
218
|
+
- **`path`** *[String, Symbol, Array<String, Symbol>]* - the path to use to extract the Tailwind CSS classnames.
|
219
|
+
- **`options[:raise]**` [Boolean] - raise a `IndexError` exception when the path is not defined in the theme.
|
220
|
+
- **`options[:object]** [Object] - the object to apply the theme to based on the object's methods.
|
221
|
+
- **`options[:attributes]**` [Hash<String, Symbol>] - the attribute hash to apply the theme to; `options[:attributes]`
|
222
|
+
overrides `options[:object]` method values if both are defined.
|
223
|
+
|
224
|
+
Raises:
|
225
|
+
- `IndexError` if `options[:raise]` is `true` and the `path` cannot be found.
|
226
|
+
- `ArgumentError` if `path` resolves to a [Hash] and `options[:object]` or `options[:attributes]` is not defined.
|
227
|
+
|
228
|
+
Returns:
|
229
|
+
- The theme classnames if `path` exists.
|
230
|
+
- `"missing-[path seperated by '-']` if `path` does not exist.
|
231
|
+
|
232
|
+
### `css!(path, options = {})`
|
233
|
+
|
234
|
+
The same as [`css`](#csspath-options), except raise an `IndexError` if the `path` cannot be found.
|
235
|
+
|
236
|
+
### `merge_css(paths, options = {})`
|
237
|
+
|
238
|
+
Resolves multiple paths and merges them together.
|
239
|
+
|
240
|
+
Arguments:
|
241
|
+
- **`paths`** [Array<String, Symbol, Array<String, Symbol>>] - the paths to use to extract the Tailwind CSS classnames.
|
242
|
+
- **`options[:raise]**` [Boolean] - raise a `IndexError` exception when the path is not defined in the theme.
|
243
|
+
- **`options[:object]** [Object] - the object to apply the theme to based on the object's methods.
|
244
|
+
- **`options[:attributes]**` [Hash<String, Symbol>] - the attribute hash to apply the theme to; `options[:attributes]`
|
245
|
+
overrides `options[:object]` method values if both are defined.
|
246
|
+
|
247
|
+
Raises:
|
248
|
+
- `IndexError` if `options[:raise]` is `true` and the `path` cannot be found.
|
249
|
+
- `ArgumentError` if `path` resolves to a [Hash] and `options[:object]` or `options[:attributes]` is not defined.
|
250
|
+
|
251
|
+
Returns a string of all the classnames that resolved to the paths given.
|
252
|
+
|
253
|
+
### `merge_css!(paths, options = {})`
|
254
|
+
|
255
|
+
The same as [`merge_css`](#merge_csspaths-options--), except raise an `IndexError` if a path cannot be found.
|
256
|
+
|
257
|
+
### `key?(path)`
|
258
|
+
|
259
|
+
Get if a `path` exists.
|
260
|
+
|
261
|
+
Arguments:
|
262
|
+
- **`path`** [String, Symbol, Array<String, Symbol] - the path defined in the theme.
|
263
|
+
|
264
|
+
Returns `true` if the `path` exists.
|
265
|
+
|
266
|
+
### `[](path)`
|
267
|
+
|
268
|
+
Return the value at the path without processing or merging the classnames.
|
269
|
+
|
270
|
+
Arguments:
|
271
|
+
- **`path`** [String, Symbol, Array<String, Symbol] - the path defined in the theme.
|
272
|
+
|
273
|
+
Returns:
|
274
|
+
- [String, Hash] if the `path` can be found.
|
275
|
+
- [NilClass] if the `path` cannot be found.
|
276
|
+
|
277
|
+
## Configuration
|
278
|
+
|
279
|
+
### Override the missing classname if a path cannot be found
|
280
|
+
|
281
|
+
Using a string:
|
282
|
+
|
283
|
+
```ruby
|
284
|
+
TailwindTheme.missing_classname = "missing"
|
285
|
+
```
|
286
|
+
|
287
|
+
Using a Proc:
|
288
|
+
|
289
|
+
```ruby
|
290
|
+
TailwindTheme.missing_classname = ->(paths) { "oops-#{paths.join("-")}-missing" }
|
291
|
+
```
|
292
|
+
|
293
|
+
Disabling:
|
294
|
+
|
295
|
+
```ruby
|
296
|
+
TailwindTheme.missing_classname = false
|
297
|
+
```
|
298
|
+
|
299
|
+
### Using a custom `tailwind_merge` instance
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
@merger = TailwindMerge::Merger.new
|
303
|
+
TailwindTheme.merger = @merger
|
304
|
+
```
|
305
|
+
|
306
|
+
### Using a custom `tailwind_merge` configuration
|
307
|
+
|
308
|
+
```ruby
|
309
|
+
TailwindTheme.merger_config = {
|
310
|
+
# Configuration
|
311
|
+
}
|
312
|
+
|
313
|
+
```
|
314
|
+
|
315
|
+
See https://github.com/gjtorikian/tailwind_merge?tab=readme-ov-file#configuration for more details.
|
316
|
+
|
317
|
+
## Development
|
318
|
+
|
319
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
320
|
+
Then, run `rake spec` to run the tests. You can also run `bin/console`
|
321
|
+
for an interactive prompt that will allow you to experiment.
|
322
|
+
|
323
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
324
|
+
To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`,
|
325
|
+
which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file
|
326
|
+
to [rubygems.org](https://rubygems.org).
|
327
|
+
|
328
|
+
## Contributing
|
329
|
+
|
330
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/jefawks3/tailwind_theme.
|
331
|
+
|
332
|
+
## License
|
333
|
+
|
334
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,281 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "erb"
|
5
|
+
require "tailwind_merge"
|
6
|
+
|
7
|
+
require_relative "tailwind_theme/version"
|
8
|
+
|
9
|
+
# Namespace for the tailwind_theme code
|
10
|
+
module TailwindTheme
|
11
|
+
class << self
|
12
|
+
# Writers for shared global objects
|
13
|
+
attr_writer :merger, :merger_config, :missing_classname
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.merger_config
|
17
|
+
@merger_config ||= {}
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the global [TailwindMerge::Merge](https://github.com/gjtorikian/tailwind_merge) merge object
|
21
|
+
# using the global `merge_config` object.
|
22
|
+
def self.merger
|
23
|
+
@merger ||= TailwindMerge::Merger.new(config: merger_config)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Loads the YAML theme file.
|
27
|
+
#
|
28
|
+
# @example
|
29
|
+
# TailwindTheme.load_file("path/to/theme.yml")
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# TailwindTheme.load_file("path/to/theme.yml.erb")
|
33
|
+
#
|
34
|
+
# @return [TailwindTheme::Theme]
|
35
|
+
def self.load_file(path)
|
36
|
+
contents = File.read path
|
37
|
+
contents = ERB.new(contents).result if path.end_with?(".erb")
|
38
|
+
Theme.new YAML.safe_load(contents)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Generate the missing CSS class name from the path
|
42
|
+
# @param [Array<String>] path the missing CSS path
|
43
|
+
# @return [Nil, String] returns a nil if the global missing_classname is false, otherwise returns a string
|
44
|
+
def self.missing_classname(path)
|
45
|
+
if @missing_classname.nil? || @missing_classname == true
|
46
|
+
"missing-#{path.join "-"}"
|
47
|
+
elsif @missing_classname.is_a? Proc
|
48
|
+
@missing_classname.call path
|
49
|
+
elsif !!@missing_classname
|
50
|
+
@missing_classname.to_s
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# The Tailwind CSS Theme object
|
55
|
+
#
|
56
|
+
# @example
|
57
|
+
# TailwindTheme::Theme.new({ button: "rounded p-8 bg-blue-600 text-white hover:bg-blue-500" })
|
58
|
+
#
|
59
|
+
class Theme
|
60
|
+
# The base key name for the sub theme
|
61
|
+
BASE_KEY = "base"
|
62
|
+
# The key to use for a nil value
|
63
|
+
NIL_KEY = "nil"
|
64
|
+
|
65
|
+
def initialize(theme_hash = {})
|
66
|
+
@theme = theme_hash
|
67
|
+
end
|
68
|
+
|
69
|
+
# Get the merged Tailwind CSS classes
|
70
|
+
#
|
71
|
+
# @param [String, Symbol, Array<String, Symbol>] path the path to the css classes or sub theme
|
72
|
+
# @param [Hash<Symbol, Object>] options the options to use when parsing the css theme
|
73
|
+
# @option options [Boolean] raise raise an `IndexError` if the `path` does not exist
|
74
|
+
# @option options [Object] object the object to apply the sub theme
|
75
|
+
# @option options [Hash<String, Object>] attributes the attributes to apply the sub theme. Overrides
|
76
|
+
# the attributes defined in `options[:object]`.
|
77
|
+
#
|
78
|
+
# Default options are:
|
79
|
+
# :raise => false
|
80
|
+
#
|
81
|
+
# @return [String] the merged Tailwind CSS classes
|
82
|
+
#
|
83
|
+
# @raise [IndexError] if the :raise options is true and the path cannot be found
|
84
|
+
# @raise [ArgumentError] if processing an object theme and the object or attributes option is not defined
|
85
|
+
def css(path, options = {})
|
86
|
+
classnames = build path, options
|
87
|
+
merge classnames
|
88
|
+
end
|
89
|
+
|
90
|
+
# Get the merged Tailwind CSS classes. Raises an IndexError if the path cannot be found.
|
91
|
+
#
|
92
|
+
# @param [String, Symbol, Array<String, Symbol>] path the path to the css classes or sub theme
|
93
|
+
# @param [Hash<Symbol, Object>] options the options to use when parsing the css theme
|
94
|
+
# @option options [Object] object the object to apply the sub theme
|
95
|
+
# @option options [Hash<String, Object>] attributes the attributes to apply the sub theme. Overrides
|
96
|
+
# the attributes defined in `options[:object]`.
|
97
|
+
#
|
98
|
+
# @return [String] the merged Tailwind CSS classes
|
99
|
+
#
|
100
|
+
# @raise [IndexError] if the :raise options is true and the path cannot be found
|
101
|
+
# @raise [ArgumentError] if processing an object theme and the object or attributes option is not defined
|
102
|
+
def css!(path, options = {})
|
103
|
+
css path, options.merge(raise: true)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Combine multiple paths and merging the combined Tailwind CSS classes.
|
107
|
+
#
|
108
|
+
# @param [Array<String, Symbol, Array<String, Symbol>>] paths the array of paths to combine
|
109
|
+
# @param [Hash<Symbol, Object>] options the options to use when parsing the css theme
|
110
|
+
# @option options [Boolean] raise raise an `IndexError` if the a path does not exist
|
111
|
+
# @option options [Object] object the object to apply the sub theme
|
112
|
+
# @option options [Hash<String, Object>] attributes the attributes to apply the sub theme. Overrides
|
113
|
+
# the attributes defined in `options[:object]`.
|
114
|
+
#
|
115
|
+
# Default options are:
|
116
|
+
# :raise => false
|
117
|
+
#
|
118
|
+
# @return [String] the merged Tailwind CSS classes
|
119
|
+
#
|
120
|
+
# @raise [IndexError] if the :raise options is true and a path cannot be found
|
121
|
+
# @raise [ArgumentError] if processing an object theme and the object or attributes option is not defined
|
122
|
+
def merge_css(paths, options = {})
|
123
|
+
classnames = paths.map { |path| build path, options }.compact.join(" ")
|
124
|
+
merge classnames
|
125
|
+
end
|
126
|
+
|
127
|
+
# Combine multiple paths and merging the combined Tailwind CSS classes. Raises an IndexError if a path
|
128
|
+
# cannot be found.
|
129
|
+
#
|
130
|
+
# @param [Array<String, Symbol, Array<String, Symbol>>] paths the array of paths to combine
|
131
|
+
# @param [Hash<Symbol, Object>] options the options to use when parsing the css theme
|
132
|
+
# @option options [Object] object the object to apply the sub theme
|
133
|
+
# @option options [Hash<String, Object>] attributes the attributes to apply the sub theme. Overrides
|
134
|
+
# the attributes defined in `options[:object]`.
|
135
|
+
#
|
136
|
+
# @return [String] the merged Tailwind CSS classes
|
137
|
+
#
|
138
|
+
# @raise [IndexError] if the :raise options is true and a path cannot be found
|
139
|
+
# @raise [ArgumentError] if processing an object theme and the object or attributes option is not defined
|
140
|
+
def merge_css!(paths, options = {})
|
141
|
+
merge_css paths, options.merge(raise: true)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Returns if the path exists
|
145
|
+
# @return [Boolean] returns true if the path exists
|
146
|
+
def key?(path)
|
147
|
+
path = normalize_path path
|
148
|
+
!!lookup_path(path, raise: false)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Lookup the raw value of the path
|
152
|
+
# @param [String, Symbol, Array[String, Symbol]] path the path to the css classes
|
153
|
+
# @return [String, Hash, NilClass]
|
154
|
+
def [](path)
|
155
|
+
path = normalize_path path
|
156
|
+
lookup_path path, raise: false
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
def normalize_path(path)
|
162
|
+
Array(path).map { |k| k.to_s.split "." }.flatten.map(&:to_s)
|
163
|
+
end
|
164
|
+
|
165
|
+
def normalize_options(options)
|
166
|
+
options.transform_keys!(&:to_sym)
|
167
|
+
options[:attributes]&.transform_keys!(&:to_s)
|
168
|
+
options
|
169
|
+
end
|
170
|
+
|
171
|
+
def lookup_path(path, options)
|
172
|
+
theme = path.empty? ? @theme : @theme.dig(*path)
|
173
|
+
raise IndexError, "theme key missing: \"#{path.join "."}\"" if options[:raise] && theme.nil?
|
174
|
+
|
175
|
+
theme
|
176
|
+
end
|
177
|
+
|
178
|
+
def build(path, options)
|
179
|
+
path = normalize_path path
|
180
|
+
options = normalize_options options
|
181
|
+
value = lookup_path path, options
|
182
|
+
return TailwindTheme.missing_classname(path) unless value
|
183
|
+
|
184
|
+
process value, options
|
185
|
+
end
|
186
|
+
|
187
|
+
def process(value, options)
|
188
|
+
case value
|
189
|
+
when Array
|
190
|
+
process_array value, options
|
191
|
+
when Hash
|
192
|
+
process_object_theme value, options
|
193
|
+
else
|
194
|
+
value
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def process_array(themes, options)
|
199
|
+
themes.map { |theme| process theme, options }
|
200
|
+
end
|
201
|
+
|
202
|
+
def process_object_theme(object_theme, options)
|
203
|
+
unless options[:object] || options[:attributes]
|
204
|
+
raise ArgumentError, "Must pass the 'object' or 'attributes' option when applying an object theme."
|
205
|
+
end
|
206
|
+
|
207
|
+
object_theme.each_with_object([object_theme[BASE_KEY]]) do |(key, value), classname_list|
|
208
|
+
if (classnames = process_object_theme_attr(key, value, options))
|
209
|
+
classname_list << classnames
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def process_object_theme_attr(attribute, object_theme, options)
|
215
|
+
case object_theme
|
216
|
+
when Array
|
217
|
+
process_object_theme_array_attr(attribute, object_theme, options)
|
218
|
+
when Hash
|
219
|
+
process_object_theme_hash_attr(attribute, object_theme, options)
|
220
|
+
else
|
221
|
+
process_object_theme_string_attr(attribute, object_theme, options)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def process_object_theme_array_attr(attribute, object_theme, options)
|
226
|
+
object_theme.map { |item| process_object_theme_attr attribute, item, options }
|
227
|
+
end
|
228
|
+
|
229
|
+
def process_object_theme_hash_attr(attribute, object_theme, options)
|
230
|
+
if process_from_attributes?(attribute, options)
|
231
|
+
process_attr_value(object_theme, attributes_value(attribute, options), options)
|
232
|
+
elsif process_from_object?(attribute, options)
|
233
|
+
process_attr_value(object_theme, object_value(attribute, options), options)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def process_object_theme_string_attr(attribute, object_theme, options)
|
238
|
+
if process_from_attributes?(attribute, options)
|
239
|
+
object_theme if attributes_value(attribute, options)
|
240
|
+
elsif process_from_object?(attribute, options)
|
241
|
+
object_theme if object_value(attribute, options)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def process_from_attributes?(attribute, options)
|
246
|
+
options[:attributes]&.key?(attribute)
|
247
|
+
end
|
248
|
+
|
249
|
+
def process_from_object?(attribute, options)
|
250
|
+
options[:object].respond_to?(attribute, true)
|
251
|
+
end
|
252
|
+
|
253
|
+
def attributes_value(attribute, options)
|
254
|
+
options.dig :attributes, attribute
|
255
|
+
end
|
256
|
+
|
257
|
+
def object_value(attribute, options)
|
258
|
+
options[:object].send attribute
|
259
|
+
end
|
260
|
+
|
261
|
+
def process_attr_value(object_theme, value, options)
|
262
|
+
key = value_to_key value
|
263
|
+
[object_theme[BASE_KEY], process(object_theme[key], options)]
|
264
|
+
end
|
265
|
+
|
266
|
+
def value_to_key(value)
|
267
|
+
if value.nil?
|
268
|
+
NIL_KEY
|
269
|
+
elsif value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
270
|
+
value
|
271
|
+
else
|
272
|
+
value.to_s
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def merge(classnames)
|
277
|
+
classnames = Array(classnames).flatten.compact.join(" ")
|
278
|
+
TailwindTheme.merger.merge classnames
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
basic: "basic-classes"
|
2
|
+
multipart:
|
3
|
+
key: "multi-part-key"
|
4
|
+
complex:
|
5
|
+
base: "complex-base"
|
6
|
+
value_with_theme:
|
7
|
+
base: "variant-base"
|
8
|
+
default:
|
9
|
+
base: "variant-default-base"
|
10
|
+
disabled: "variant-default-disabled"
|
11
|
+
missing_style:
|
12
|
+
base: "missing-style-base"
|
13
|
+
foo: "missing-foo"
|
14
|
+
values:
|
15
|
+
base: "values-base"
|
16
|
+
test: "values-test"
|
17
|
+
foo: "values-foo"
|
18
|
+
bar: "values-bar"
|
19
|
+
boolean:
|
20
|
+
base: "boolean"
|
21
|
+
true: "boolean-true"
|
22
|
+
false: "boolean-false"
|
23
|
+
missing: "missing-attribute"
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tailwind_theme"
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
# Enable flags like --only-failures and --next-failure
|
7
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
8
|
+
|
9
|
+
# Disable RSpec exposing methods globally on `Module` and `main`
|
10
|
+
config.disable_monkey_patching!
|
11
|
+
|
12
|
+
config.expect_with :rspec do |c|
|
13
|
+
c.syntax = :expect
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
require "ostruct"
|
5
|
+
|
6
|
+
RSpec.describe TailwindTheme::Theme do
|
7
|
+
subject(:theme) { TailwindTheme.load_file theme_file }
|
8
|
+
|
9
|
+
let(:theme_file) { File.expand_path("../fixtures/test-theme.yml", __dir__) }
|
10
|
+
|
11
|
+
let(:test_object) do
|
12
|
+
OpenStruct.new values: :foo, value_with_theme: :missing, boolean: false, foo: :bar
|
13
|
+
end
|
14
|
+
|
15
|
+
describe ".css" do
|
16
|
+
it "raises an IndexError on not found and raise options is true" do
|
17
|
+
expect { theme.css!("test.path", raise: true) }.to raise_error IndexError
|
18
|
+
end
|
19
|
+
|
20
|
+
it "resolves an array path" do
|
21
|
+
expect(theme.css(%i[multipart key])).to eq "multi-part-key"
|
22
|
+
end
|
23
|
+
|
24
|
+
it "resolves an string path" do
|
25
|
+
expect(theme.css("multipart.key")).to eq "multi-part-key"
|
26
|
+
end
|
27
|
+
|
28
|
+
context "when string css path" do
|
29
|
+
it "returns a css string" do
|
30
|
+
expect(theme.css("basic")).to eq "basic-classes"
|
31
|
+
end
|
32
|
+
|
33
|
+
it "returns a missing css string" do
|
34
|
+
expect(theme.css("foo.bar")).to eq "missing-foo-bar"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context "when object theme path" do
|
39
|
+
it "raises ArgumentError when object and attribute option is not defined" do
|
40
|
+
expect { theme.css(:complex) }.to raise_error ArgumentError
|
41
|
+
end
|
42
|
+
|
43
|
+
it "returns the css classnames for an object" do
|
44
|
+
expect(theme.css(:complex, object: test_object))
|
45
|
+
.to eq "complex-base variant-base values-base values-foo boolean boolean-false"
|
46
|
+
end
|
47
|
+
|
48
|
+
it "returns the css classnames for given attributes" do
|
49
|
+
expect(theme.css(:complex, attributes: { boolean: false }))
|
50
|
+
.to eq "complex-base boolean boolean-false"
|
51
|
+
end
|
52
|
+
|
53
|
+
it "does not have the missing attribute classname" do
|
54
|
+
expect(theme.css(:complex, object: test_object)).not_to include("missing-attribute")
|
55
|
+
end
|
56
|
+
|
57
|
+
it "overrides the object attribute value" do
|
58
|
+
expect(theme.css(:complex, object: test_object, attributes: { boolean: true }))
|
59
|
+
.to eq "complex-base variant-base values-base values-foo boolean boolean-true"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe ".css!" do
|
65
|
+
it "calls the css method with raise: true option" do
|
66
|
+
expect { theme.css!("test.path") }.to raise_error IndexError
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe ".merge_css" do
|
71
|
+
it "returns the merged paths" do
|
72
|
+
expect(theme.merge_css(%w[basic complex], object: test_object))
|
73
|
+
.to eq "basic-classes complex-base variant-base values-base values-foo boolean boolean-false"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe ".merge_css!" do
|
78
|
+
it "calls the css method with raise: true option" do
|
79
|
+
expect { theme.merge_css!(["test.path", :complex]) }.to raise_error IndexError
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe ".key?" do
|
84
|
+
it "returns true when key is defined" do
|
85
|
+
expect(theme).to be_key :complex
|
86
|
+
end
|
87
|
+
|
88
|
+
it "returns false when key is not defined" do
|
89
|
+
expect(theme).not_to be_key %i[foo bar]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe TailwindTheme do
|
4
|
+
it "has a version number" do
|
5
|
+
expect(TailwindTheme::VERSION).not_to be nil
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "#load_file" do
|
9
|
+
let(:theme_file) { File.expand_path("fixtures/test-theme.yml", __dir__) }
|
10
|
+
|
11
|
+
it "returns the Theme from a file" do
|
12
|
+
expect(described_class.load_file(theme_file)).to be_kind_of(TailwindTheme::Theme)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "#missing_classname" do
|
17
|
+
subject(:missing_classname) { described_class.missing_classname %w[test path] }
|
18
|
+
|
19
|
+
context "when self.missing_classname = nil" do
|
20
|
+
before { described_class.missing_classname = nil }
|
21
|
+
|
22
|
+
it "returns the default missing classname" do
|
23
|
+
expect(missing_classname).to eq "missing-test-path"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context "when self.missing_classname = true" do
|
28
|
+
before { described_class.missing_classname = true }
|
29
|
+
|
30
|
+
it "returns the default missing classname" do
|
31
|
+
expect(missing_classname).to eq "missing-test-path"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "when self.missing_classname = false" do
|
36
|
+
before { described_class.missing_classname = false }
|
37
|
+
|
38
|
+
it "returns the default missing classname" do
|
39
|
+
expect(missing_classname).to be_nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context "when self.missing_classname = :custom_class" do
|
44
|
+
before { described_class.missing_classname = :custom_class }
|
45
|
+
|
46
|
+
it "returns 'custom_class'" do
|
47
|
+
expect(missing_classname).to eq "custom_class"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "when self.missing_classname = 'custom-class'" do
|
52
|
+
before { described_class.missing_classname = "custom-class" }
|
53
|
+
|
54
|
+
it "returns 'custom_class'" do
|
55
|
+
expect(missing_classname).to eq "custom-class"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context "when self.missing_classname = Proc" do
|
60
|
+
before { described_class.missing_classname = ->(path) { "oops-#{path.join "-"}-missing" } }
|
61
|
+
|
62
|
+
it "returns 'custom_class'" do
|
63
|
+
expect(missing_classname).to eq "oops-test-path-missing"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tailwind_theme
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- James Fawks
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-01-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: tailwind_merge
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.10'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.10'
|
27
|
+
description:
|
28
|
+
email:
|
29
|
+
- hello@jfawks.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- LICENSE
|
35
|
+
- README.md
|
36
|
+
- lib/tailwind_theme.rb
|
37
|
+
- lib/tailwind_theme/version.rb
|
38
|
+
- spec/fixtures/test-theme.yml
|
39
|
+
- spec/spec_helper.rb
|
40
|
+
- spec/tailwind_theme/theme_spec.rb
|
41
|
+
- spec/tailwind_theme_spec.rb
|
42
|
+
homepage: https://github.com/jefawks3
|
43
|
+
licenses:
|
44
|
+
- MIT
|
45
|
+
metadata:
|
46
|
+
homepage_uri: https://github.com/jefawks3
|
47
|
+
source_code_uri: https://github.com/jefawks3
|
48
|
+
changelog_uri: https://github.com/jefawks3/releases
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options: []
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: 2.6.0
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
requirements: []
|
64
|
+
rubygems_version: 3.4.10
|
65
|
+
signing_key:
|
66
|
+
specification_version: 4
|
67
|
+
summary: A small Tailwind CSS theme utility to read and apply themes stored in a YAML
|
68
|
+
file.
|
69
|
+
test_files:
|
70
|
+
- spec/fixtures/test-theme.yml
|
71
|
+
- spec/spec_helper.rb
|
72
|
+
- spec/tailwind_theme/theme_spec.rb
|
73
|
+
- spec/tailwind_theme_spec.rb
|