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 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TailwindTheme
4
+ VERSION = "0.1.0"
5
+ end
@@ -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"
@@ -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