tailwind_theme 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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