lycan_ui 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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +33 -0
  4. data/Rakefile +5 -0
  5. data/lib/generators/lycan_ui/add_generator.rb +109 -0
  6. data/lib/generators/lycan_ui/setup_generator.rb +76 -0
  7. data/lib/generators/lycan_ui/templates/components/accordion.rb +63 -0
  8. data/lib/generators/lycan_ui/templates/components/alert.rb +35 -0
  9. data/lib/generators/lycan_ui/templates/components/avatar.rb +38 -0
  10. data/lib/generators/lycan_ui/templates/components/badge.rb +29 -0
  11. data/lib/generators/lycan_ui/templates/components/button.rb +49 -0
  12. data/lib/generators/lycan_ui/templates/components/checkbox.rb +31 -0
  13. data/lib/generators/lycan_ui/templates/components/collapsible.rb +40 -0
  14. data/lib/generators/lycan_ui/templates/components/component.rb +72 -0
  15. data/lib/generators/lycan_ui/templates/components/dialog.rb +129 -0
  16. data/lib/generators/lycan_ui/templates/components/dropdown.rb +242 -0
  17. data/lib/generators/lycan_ui/templates/components/input.rb +26 -0
  18. data/lib/generators/lycan_ui/templates/components/label.rb +30 -0
  19. data/lib/generators/lycan_ui/templates/components/popover.rb +53 -0
  20. data/lib/generators/lycan_ui/templates/components/radio.rb +27 -0
  21. data/lib/generators/lycan_ui/templates/components/select.rb +38 -0
  22. data/lib/generators/lycan_ui/templates/components/switch.rb +26 -0
  23. data/lib/generators/lycan_ui/templates/components/textarea.rb +25 -0
  24. data/lib/generators/lycan_ui/templates/extras/form_builder.rb +90 -0
  25. data/lib/generators/lycan_ui/templates/javascript/accordion_controller.js +46 -0
  26. data/lib/generators/lycan_ui/templates/javascript/avatar_controller.js +34 -0
  27. data/lib/generators/lycan_ui/templates/javascript/collapsible_controller.js +23 -0
  28. data/lib/generators/lycan_ui/templates/javascript/dialog_controller.js +90 -0
  29. data/lib/generators/lycan_ui/templates/javascript/dropdown_controller.js +395 -0
  30. data/lib/generators/lycan_ui/templates/javascript/popover_controller.js +114 -0
  31. data/lib/generators/lycan_ui/templates/setup/application.tailwind.css +94 -0
  32. data/lib/generators/lycan_ui/templates/setup/lycan_ui_helper.rb +39 -0
  33. data/lib/lycan_ui/configuration.rb +32 -0
  34. data/lib/lycan_ui/railtie.rb +6 -0
  35. data/lib/lycan_ui/version.rb +3 -0
  36. data/lib/lycan_ui.rb +8 -0
  37. data/lib/tasks/lycan_ui_tasks.rake +6 -0
  38. metadata +107 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fa1792973d0fd046ee21833d5608a50aede8f258be1673ab775b8c19f76212d6
4
+ data.tar.gz: 6fb5d6c3b5427198226653455564c74ef2f3f292680d1f85943d13ab34b93d2c
5
+ SHA512:
6
+ metadata.gz: 2afc70394bd910943a561bde1f909080748c57d7fca4fe9b83a9851aedebcab1dd319ef16469109e83b6f5c5185d72148761b0818cd7b38b31c482faeca8bb62
7
+ data.tar.gz: cede02b1b57b0f5055b878dd0e66c660a87b969f5810ada54db4ea283313bbbd1ac3a7c31e33d2f4020509b30665da1b5e65d9bca62e91005b012b071d6e0741
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Ethan Kircher
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # LycanUi
2
+
3
+ Accessible, open source, and customizable ui components for your Rails app.
4
+
5
+ ## Installation
6
+
7
+ Add the gem to the `development` group in your `Gemfile`,
8
+
9
+ ```ruby
10
+ group :development do
11
+ gem 'lycan_ui', github: 'MSILycanthropy/lycan_ui', require: false
12
+ end
13
+ ```
14
+
15
+ Run `bundle install` and then run the setup script with,
16
+
17
+ ```
18
+ rails generate lycan_ui:setup
19
+ ```
20
+
21
+ Follow the prompts in your terminal and then.. you're done!
22
+
23
+ ## Documentation
24
+
25
+ Visit http://ui.lycanthropy.dev to view documentation.
26
+
27
+ ## Contributing
28
+
29
+ Please read the [contributing guide](/CONTRIBUTING.md).
30
+
31
+ ## License
32
+
33
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+
5
+ require "bundler/gem_tasks"
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lycan_ui/configuration"
4
+
5
+ module LycanUi
6
+ module Generators
7
+ class AddGenerator < Rails::Generators::NamedBase
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ REQUIRES_FLOATING = [ "dropdown", "popover" ].freeze
11
+ REQUIRES_FOCUS_TRAP = [ "popover" ].freeze
12
+
13
+ alias_method :component, :file_name
14
+
15
+ def load_configuration
16
+ Configuration.setup
17
+ end
18
+
19
+ def install_form
20
+ return unless component == "form"
21
+
22
+ copy_file("extras/form_builder.rb", "app/helpers/lycan_ui/form_helper.rb")
23
+
24
+ [ "label", "input", "textarea", "checkbox", "button", "radio", "switch" ].each do |comp|
25
+ puts "Installing #{comp.titleize}..."
26
+ %x(rails g lycan_ui:add #{comp} --force)
27
+ end
28
+
29
+ exit
30
+ end
31
+
32
+ def copy_views
33
+ copy_file("components/#{component}.rb", "app/components/lycan_ui/#{component}.rb")
34
+ end
35
+
36
+ def copy_js
37
+ if file_exists?("javascript/#{component}_controller.js")
38
+ copy_file(
39
+ "javascript/#{component}_controller.js",
40
+ "#{Configuration.javascript_dir}/#{component}_controller.js",
41
+ )
42
+ end
43
+
44
+ if dir_exists?("javascript/#{component}")
45
+ directory("javascript/#{component}", "#{Configuration.javascript_dir}/#{component}")
46
+ end
47
+
48
+ unless importmaps?
49
+ content = <<~JS
50
+
51
+ import #{component.titleize}Controller from "./#{component}_controller"
52
+ application.register("#{component}", #{component.titleize}Controller)
53
+ JS
54
+ append_to_file("#{Configuration.javascript_dir}/index.js", content)
55
+ end
56
+ end
57
+
58
+ def install_focus_trap
59
+ return unless focus_trapping?
60
+
61
+ js_command = if importmaps?
62
+ "bin/importmap pin focus-trap"
63
+ else
64
+ "yarn add focus-trap"
65
+ end
66
+
67
+ run(js_command)
68
+ end
69
+
70
+ def install_floating
71
+ return unless floating?
72
+
73
+ js_command = if importmaps?
74
+ "bin/importmap pin @floating-ui/dom"
75
+ else
76
+ "yarn add @floating-ui/dom"
77
+ end
78
+
79
+ run(js_command)
80
+ end
81
+
82
+ private
83
+
84
+ def focus_trapping?
85
+ REQUIRES_FOCUS_TRAP.include?(component)
86
+ end
87
+
88
+ def floating?
89
+ REQUIRES_FLOATING.include?(component)
90
+ end
91
+
92
+ def dir_exists?(name)
93
+ root = source_paths.first
94
+
95
+ Dir.exist?("#{root}/#{name}")
96
+ end
97
+
98
+ def file_exists?(name)
99
+ root = source_paths.first
100
+
101
+ File.exist?("#{root}/#{name}")
102
+ end
103
+
104
+ def importmaps?
105
+ File.exist?("#{Rails.root}/config/importmap.rb")
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-prompt"
4
+ require "lycan_ui/configuration"
5
+
6
+
7
+ module LycanUi
8
+ module Generators
9
+ class SetupGenerator < Rails::Generators::Base
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ def load_configuration
13
+ Configuration.setup
14
+ end
15
+
16
+ def install_lucide_rails
17
+ return if lucide_rails_installed?
18
+
19
+ run("bundle add lucide-rails")
20
+ end
21
+
22
+ def install_tailwind_merge
23
+ return unless tailwind?
24
+ return if tailwind_merge_installed?
25
+
26
+ run("bundle add tailwind_merge")
27
+ end
28
+
29
+ def install_tailwind_config
30
+ return unless tailwind?
31
+
32
+ template("setup/application.tailwind.css", Configuration.stylesheet, force: true)
33
+ end
34
+
35
+ def copy_helpers
36
+ copy_file("setup/lycan_ui_helper.rb", "app/helpers/lycan_ui_helper.rb")
37
+ end
38
+
39
+ # TODO: add `all`
40
+ def install_components
41
+ copy_file("components/component.rb", "app/components/lycan_ui/component.rb")
42
+
43
+ prompt = TTY::Prompt.new
44
+ path = source_paths.first + "/components"
45
+ choices = Dir.glob("#{path}/*.rb").map do |c|
46
+ c.sub(path, "").sub(".rb", "").gsub("_", " ").slice(1..).strip
47
+ end.reject { |c| c == "component" }.push("form").sort.index_by { |comp| comp.titleize }
48
+
49
+ selected = prompt.multi_select("Select your options:", choices, filter: true)
50
+
51
+ selected.each do |component|
52
+ puts "Installing #{component.titleize}..."
53
+ %x(rails g lycan_ui:add #{component} --force)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def lucide_rails_installed?
60
+ Gem.loaded_specs.key?("lucide-rails")
61
+ end
62
+
63
+ def tailwind_merge_installed?
64
+ Gem.loaded_specs.key?("tailwind_merge")
65
+ end
66
+
67
+ def tailwind?
68
+ true
69
+ end
70
+
71
+ def importmaps?
72
+ File.exist?("#{Rails.root}/config/importmap.rb")
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LycanUi
4
+ class Accordion < Component
5
+ attr_reader :name, :multiple
6
+
7
+ def initialize(multiple: false, **attributes)
8
+ @multiple = multiple
9
+
10
+ action = <<~ACTION.squish
11
+ keydown.up->accordion#focusPrevious:prevent keydown.down->accordion#focusNext:prevent
12
+ keydown.home->accordion#focusFirst:prevent keydown.end->accordion#focusLast:prevent
13
+ ACTION
14
+
15
+ super(attributes, data: { controller: "accordion", action: })
16
+ end
17
+
18
+ def template(&)
19
+ @name = lycan_ui_id unless multiple
20
+
21
+ tag.div(**attributes) { yield self }
22
+ end
23
+
24
+ ITEM_CLASSES = <<~CLASSES.squish
25
+ border-b border-surface interpolate-keywords details:h-0 details-open:h-auto
26
+ details:overflow-hidden details:text-sm details:motion-safe:transition-all
27
+ details:duration-200 details:ease-out details:transition-discrete
28
+ disabled:opacity-50 disabled:pointer-events-none
29
+ open:[&>summary>svg]:rotate-180
30
+ CLASSES
31
+
32
+ def item(open: false, **item_attributes, &)
33
+ final_attributes = merge_attributes(item_attributes, open:, class: ITEM_CLASSES, name:)
34
+
35
+ tag.details(**final_attributes, &)
36
+ end
37
+
38
+ TRIGGER_CLASSES = <<~CLASSES.squish
39
+ cursor-pointer flex flex-1 items-center justify-between py-4 font-medium
40
+ transition-all underline-offset-4 hover:underline decoration-accent
41
+ CLASSES
42
+ def trigger(content = nil, icon: "chevron-down", **trigger_attributes, &block)
43
+ final_attributes = merge_attributes(
44
+ trigger_attributes,
45
+ class: TRIGGER_CLASSES,
46
+ data: { accordion_target: "item" },
47
+ )
48
+
49
+ tag.summary(**final_attributes) do
50
+ safe_join([
51
+ determine_content(content, &block),
52
+ icon.present? ? lucide_icon(icon, class: "size-4 shrink-0 transition-transform duration-200") : nil,
53
+ ].compact)
54
+ end
55
+ end
56
+
57
+ def content(**content_attributes, &)
58
+ final_attributes = merge_attributes(content_attributes, class: "pb-4")
59
+
60
+ tag.div(**final_attributes, &)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LycanUi
4
+ class Alert < Component
5
+ DEFAULT_CLASSES = <<~CLASSES.squish
6
+ relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px]
7
+ [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-3.5
8
+ CLASSES
9
+
10
+ VARIANT_CLASSES = {
11
+ primary: "border-on-surface",
12
+ danger: "border-danger bg-danger-500 text-on-danger [&>svg]:text-on-danger",
13
+ }.freeze
14
+
15
+ def initialize(variant: :primary, **attributes)
16
+ super(attributes, role: "alert", class: [ DEFAULT_CLASSES, VARIANT_CLASSES[variant] ])
17
+ end
18
+
19
+ def template(&)
20
+ tag.div(**attributes) { yield self }
21
+ end
22
+
23
+ def title(content = nil, **title_attributes, &block)
24
+ final_attributes = merge_attributes(title_attributes, class: "mb-1 font-medium leading-none tracking-tight")
25
+
26
+ tag.div(**final_attributes) { determine_content(content, &block) }
27
+ end
28
+
29
+ def description(content = nil, **description_attributes, &block)
30
+ final_attributes = merge_attributes(description_attributes, class: "text-sm [&_p]:leading-relaxed")
31
+
32
+ tag.div(**final_attributes) { determine_content(content, &block) }
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LycanUi
4
+ class Avatar < Component
5
+ attr_reader :args, :fallback
6
+
7
+ FALLBACK_CLASSES = <<~CLASSES.squish
8
+ relative flex items-center justify-center h-10 w-10 shrink-0 overflow-hidden
9
+ rounded-full bg-secondary-400 shadow-lg
10
+ CLASSES
11
+
12
+ def initialize(*args, fallback: nil, **attributes)
13
+ @args = args
14
+ @fallback = fallback
15
+
16
+ if fallback.present?
17
+ super(attributes, class: FALLBACK_CLASSES, data: { controller: "avatar" })
18
+ else
19
+ super(attributes, class: "rounded-full h-10 w-10 object-cover")
20
+ end
21
+ end
22
+
23
+ def template
24
+ if fallback.present?
25
+ tag.div(**attributes) do
26
+ concat(tag.div(fallback, class: "text-on-secondary font-medium", data: { avatar_target: "fallback" }))
27
+ concat(image_tag(
28
+ *args,
29
+ class: "absolute w-full h-full object-cover shadow-lg",
30
+ data: { action: "error->avatar#showFallback load->avatar#hideFallback", avatar_target: "image" },
31
+ ))
32
+ end
33
+ else
34
+ image_tag(*args, **attributes)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LycanUi
4
+ class Badge < Component
5
+ attr_reader :content
6
+
7
+ DEFAULT_CLASSES = <<~CLASSES.squish
8
+ inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors
9
+ focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2
10
+ CLASSES
11
+
12
+ VARIANT_CLASSES = {
13
+ primary: "border-transparent bg-primary text-on-primary hover:bg-primary/80",
14
+ secondary: "border-transparent bg-secondary text-on-secondary hover:bg-secondary/80",
15
+ outline: "border-primary",
16
+ danger: "border-transparent bg-danger text-on-danger hover:bg-danger/80",
17
+ }.freeze
18
+
19
+ def initialize(content = nil, variant: :primary, **attributes)
20
+ @content = content
21
+
22
+ super(attributes, class: [ DEFAULT_CLASSES, VARIANT_CLASSES[variant] ])
23
+ end
24
+
25
+ def template(&block)
26
+ tag.div(**attributes) { determine_content(content, &block) }
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LycanUi
4
+ class Button < Component
5
+ attr_accessor :content, :as_child
6
+
7
+ DEFAULT_CLASSES = <<~CLASSES.squish
8
+ cursor-pointer inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium
9
+ transition-colors disabled:pointer-events-none disabled:opacity-50 ring-offset-background
10
+ focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2
11
+ CLASSES
12
+
13
+ SIZE_CLASSES = {
14
+ extra_small: "h-6 px-1 text-xs",
15
+ small: "h-7 px-2 text-sm",
16
+ medium: "h-10 px-4 text-base",
17
+ large: "h-11 px-5 text-lg",
18
+ }.freeze
19
+
20
+ VARIANT_CLASSES = {
21
+ primary:
22
+ "bg-primary text-on-primary hover:bg-primary/80 focus-visible:ring-primary",
23
+ secondary:
24
+ "bg-secondary text-on-secondary hover:bg-secondary/80 focus-visible:ring-secondary",
25
+ danger:
26
+ "bg-danger text-on-danger hover:bg-danger/80 focus-visible:ring-danger",
27
+ outline:
28
+ "border border-primary hover:bg-primary hover:text-on-primary focus-visible:ring-primary",
29
+ ghost:
30
+ "hover:bg-secondary/30 hover:text-on-secondary focus-visible:ring-secondary/30",
31
+ link:
32
+ "underline-offset-4 decoration-accent hover:underline focus-visible:ring-accent",
33
+ none: nil,
34
+ }.freeze
35
+
36
+ def initialize(content = nil, size: :medium, variant: :primary, as_child: false, **attributes)
37
+ @content = content
38
+ @as_child = as_child
39
+
40
+ super(attributes, type: :button, class: [ DEFAULT_CLASSES, VARIANT_CLASSES[variant], SIZE_CLASSES[size] ])
41
+ end
42
+
43
+ def template(&block)
44
+ return yield attributes.except(:type) if as_child
45
+
46
+ tag.button(**attributes) { determine_content(content, &block) }
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LycanUi
4
+ class Checkbox < Component
5
+ attr_reader :object_name, :method, :checked_value, :unchecked_value
6
+
7
+ DEFAULT_CLASSES = <<~CLASSES.squish
8
+ appearance-none size-4 border border-accent rounded motion-safe:transition-all
9
+ ease-[ease] relative cursor-pointer
10
+ after:top-[45%] after:left-1/2 after:w-1.5 after:h-2.5 after:opacity-0
11
+ checked:bg-accent checked:border-transparent checked:scale-110 after:absolute
12
+ after:border-r-2 after:border-b-2 after:border-surface-50 dark:after:border-surface-950
13
+ after:-translate-x-1/2 after:-translate-y-1/2 after:rotate-45 after:scale-0
14
+ after:transition-transform checked:after:scale-100 checked:after:opacity-100
15
+ disabled:opacity-65 disabled:cursor-not-allowed
16
+ CLASSES
17
+
18
+ def initialize(object_name, method, options = {}, checked_value = "1", unchecked_value = "0")
19
+ @object_name = object_name
20
+ @method = method
21
+ @checked_value = checked_value
22
+ @unchecked_value = unchecked_value
23
+
24
+ super(options, class: DEFAULT_CLASSES)
25
+ end
26
+
27
+ def template
28
+ checkbox(object_name, method, attributes, checked_value, unchecked_value)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LycanUi
4
+ class Collapsible < Component
5
+ def initialize(**attributes)
6
+ super(attributes, data: { controller: "collapsible" })
7
+ end
8
+
9
+ def template(&)
10
+ @id = lycan_ui_id
11
+
12
+ tag.div(**attributes) { yield self }
13
+ end
14
+
15
+ def trigger(content = nil, **trigger_attributes, &)
16
+ final_attributes = merge_attributes(
17
+ trigger_attributes,
18
+ aria: { controls: @id, expanded: false },
19
+ data: { collapsible_target: "trigger", action: "collapsible#toggle" },
20
+ )
21
+
22
+ ui.button(content, **final_attributes, &)
23
+ end
24
+
25
+ CONTENT_CLASSES = <<~CLASSES
26
+ overflow-hidden interpolate-keywords transition-discrete starting:h-0 hidden:h-0 h-auto motion-safe:transition-all duration-300
27
+ CLASSES
28
+ def content(**content_attributes, &)
29
+ final_attributes = merge_attributes(
30
+ content_attributes,
31
+ id: @id,
32
+ class: CONTENT_CLASSES,
33
+ hidden: true,
34
+ data: { collapsible_target: "content" },
35
+ )
36
+
37
+ tag.div(**final_attributes, &)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LycanUi
4
+ class Component
5
+ attr_accessor :attributes
6
+ delegate_missing_to :@view_context
7
+
8
+ def initialize(attributes = {}, **default_attributes)
9
+ @attributes = merge_attributes(attributes, **default_attributes)
10
+ end
11
+
12
+ def render_in(view_context, &)
13
+ @view_context = view_context
14
+
15
+ template(&)
16
+ ensure
17
+ @view_context = nil
18
+ end
19
+
20
+ def template
21
+ raise NotImplementedError
22
+ end
23
+
24
+ private
25
+
26
+ def merge_attributes(*incoming, **default)
27
+ default.merge(*incoming) do |key, default_value, incoming_value|
28
+ case key
29
+ when :class
30
+ merge_classes(default_value, incoming_value)
31
+ when :data
32
+ merge_data(default_value, incoming_value)
33
+ when :aria
34
+ merge_aria(default_value, incoming_value)
35
+ else
36
+ incoming_value
37
+ end
38
+ end
39
+ end
40
+
41
+ def merge_classes(*classes)
42
+ @@merger ||= TailwindMerge::Merger.new
43
+ @@merger.merge(classes)
44
+ end
45
+
46
+ def merge_data(*hashes)
47
+ hashes.compact.each_with_object({}) do |hash, result|
48
+ result.deep_merge(hash) do |key, old_value, new_value|
49
+ case key
50
+ when :action, :controller
51
+ [ old_value, new_value ].compact.join(" ")
52
+ else
53
+ new_value
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def merge_aria(*hashes)
60
+ hashes.compact.each_with_object({}) do |hash, result|
61
+ result.deep_merge(hash)
62
+ end
63
+ end
64
+
65
+ def determine_content(text = nil, &)
66
+ return text if text.present?
67
+ return unless block_given?
68
+
69
+ @view_context.capture(&)
70
+ end
71
+ end
72
+ end