loco_motion-rails 0.4.0 → 0.5.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -1
  3. data/app/components/daisy/actions/button_component.html.haml +2 -2
  4. data/app/components/daisy/actions/button_component.rb +98 -59
  5. data/app/components/daisy/actions/dropdown_component.html.haml +1 -2
  6. data/app/components/daisy/actions/dropdown_component.rb +7 -10
  7. data/app/components/daisy/actions/modal_component.html.haml +10 -8
  8. data/app/components/daisy/actions/modal_component.rb +6 -6
  9. data/app/components/daisy/actions/swap_component.rb +13 -9
  10. data/app/components/daisy/actions/theme_controller.js +113 -0
  11. data/app/components/daisy/actions/theme_controller_component.rb +58 -17
  12. data/app/components/daisy/actions/theme_preview_component.html.haml +5 -0
  13. data/app/components/daisy/actions/theme_preview_component.rb +68 -0
  14. data/app/components/daisy/data_display/accordion_component.html.haml +0 -1
  15. data/app/components/daisy/data_display/accordion_component.rb +10 -3
  16. data/app/components/daisy/data_display/avatar_component.html.haml +1 -1
  17. data/app/components/daisy/data_display/avatar_component.rb +17 -7
  18. data/app/components/daisy/data_display/badge_component.rb +122 -4
  19. data/app/components/daisy/data_display/card_component.html.haml +1 -1
  20. data/app/components/daisy/data_display/card_component.rb +20 -6
  21. data/app/components/daisy/data_display/chat_component.rb +2 -2
  22. data/app/components/daisy/data_display/collapse_component.rb +9 -5
  23. data/app/components/daisy/data_display/countdown_component.rb +15 -5
  24. data/app/components/daisy/data_display/figure_component.rb +8 -3
  25. data/app/components/daisy/data_display/kbd_component.rb +13 -4
  26. data/app/components/daisy/data_display/list_component.html.haml +5 -0
  27. data/app/components/daisy/data_display/list_component.rb +82 -0
  28. data/app/components/daisy/data_display/list_item_component.rb +39 -0
  29. data/app/components/daisy/data_display/stat_component.html.haml +5 -6
  30. data/app/components/daisy/data_display/stat_component.rb +21 -8
  31. data/app/components/daisy/data_display/status_component.rb +47 -0
  32. data/app/components/daisy/data_display/timeline_component.rb +1 -1
  33. data/app/components/daisy/data_input/cally_component.html.haml +14 -0
  34. data/app/components/daisy/data_input/cally_component.rb +182 -0
  35. data/app/components/daisy/data_input/cally_input_component.html.haml +5 -0
  36. data/app/components/daisy/data_input/cally_input_component.rb +165 -0
  37. data/app/components/daisy/data_input/cally_input_controller.js +235 -0
  38. data/app/components/daisy/data_input/checkbox_component.html.haml +20 -0
  39. data/app/components/daisy/data_input/checkbox_component.rb +21 -7
  40. data/app/components/daisy/data_input/fieldset_component.html.haml +8 -0
  41. data/app/components/daisy/data_input/fieldset_component.rb +57 -0
  42. data/app/components/daisy/data_input/file_input_component.rb +6 -0
  43. data/app/components/daisy/data_input/filter_component.html.haml +3 -0
  44. data/app/components/daisy/data_input/filter_component.rb +221 -0
  45. data/app/components/daisy/data_input/label_component.rb +2 -2
  46. data/app/components/daisy/data_input/radio_button_component.rb +1 -1
  47. data/app/components/daisy/data_input/rating_component.html.haml +0 -2
  48. data/app/components/daisy/data_input/rating_component.rb +3 -2
  49. data/app/components/daisy/data_input/select_component.html.haml +27 -15
  50. data/app/components/daisy/data_input/select_component.rb +152 -10
  51. data/app/components/daisy/data_input/text_area_component.rb +11 -8
  52. data/app/components/daisy/data_input/text_input_component.html.haml +25 -4
  53. data/app/components/daisy/data_input/text_input_component.rb +38 -36
  54. data/app/components/daisy/data_input/toggle_component.rb +12 -0
  55. data/app/components/daisy/feedback/alert_component.html.haml +1 -1
  56. data/app/components/daisy/feedback/alert_component.rb +86 -2
  57. data/app/components/daisy/feedback/loading_component.rb +10 -3
  58. data/app/components/daisy/feedback/skeleton_component.rb +1 -1
  59. data/app/components/daisy/layout/divider_component.rb +4 -2
  60. data/app/components/daisy/layout/drawer_component.html.haml +0 -1
  61. data/app/components/daisy/layout/footer_component.rb +6 -6
  62. data/app/components/daisy/mockup/device_component.rb +15 -18
  63. data/app/components/daisy/navigation/breadcrumbs_component.html.haml +0 -1
  64. data/app/components/daisy/navigation/breadcrumbs_component.rb +84 -9
  65. data/app/components/daisy/navigation/dock_component.rb +146 -0
  66. data/app/components/daisy/navigation/link_component.rb +18 -9
  67. data/app/components/daisy/navigation/menu_component.rb +15 -9
  68. data/app/components/daisy/navigation/navbar_component.html.haml +1 -1
  69. data/app/components/daisy/navigation/navbar_component.rb +2 -13
  70. data/app/components/daisy/navigation/steps_component.rb +6 -6
  71. data/app/components/daisy/navigation/tabs_component.html.haml +0 -1
  72. data/app/components/daisy/navigation/tabs_component.rb +26 -16
  73. data/app/components/hero/icon_component.rb +15 -5
  74. data/app/helpers/daisy/form_builder_helper.rb +30 -3
  75. data/app/views/examples/daisy/data_input/filters.html.haml +62 -0
  76. data/lib/hero.rb +1 -1
  77. data/lib/loco_motion/base_component.rb +44 -1
  78. data/lib/loco_motion/component_config.rb +1 -0
  79. data/lib/loco_motion/concerns/iconable_component.rb +134 -0
  80. data/lib/loco_motion/concerns/labelable_component.rb +142 -0
  81. data/lib/loco_motion/concerns/linkable_component.rb +40 -0
  82. data/lib/loco_motion/concerns/tippable_component.rb +25 -10
  83. data/lib/loco_motion/helpers.rb +27 -18
  84. data/lib/loco_motion/patches/view_component/slot_loco_parent_patch.rb +37 -0
  85. data/lib/loco_motion/patches/view_component/slotable_default_patch.rb +21 -0
  86. data/lib/loco_motion/version.rb +1 -1
  87. data/lib/loco_motion.rb +12 -2
  88. metadata +65 -19
  89. data/app/components/daisy/actions/theme_controller_component.html.haml +0 -5
  90. data/app/components/daisy/layout/artboard_component.rb +0 -59
  91. data/app/components/daisy/navigation/bottom_nav_component.rb +0 -138
@@ -8,21 +8,21 @@
8
8
  # @slot tabs+ {Daisy::Navigation::TabsComponent::TabComponent} The individual
9
9
  # tabs to display.
10
10
  #
11
- # @example Basic tabs with links
12
- # = daisy_tabs(css: "tabs-bordered") do |tabs|
11
+ # @loco_example Basic tabs with links
12
+ # = daisy_tabs(css: "tabs-border") do |tabs|
13
13
  # - tabs.with_tab(title: "Home", active: true)
14
14
  # - tabs.with_tab(title: "Click Me", html: { onclick: "alert('Clicked!')" })
15
15
  # - tabs.with_tab(title: "Google", href: "https://google.com", target: "_blank")
16
16
  #
17
- # @example Radio button tabs with content
18
- # = daisy_tabs(css: "tabs-lifted", radio: true) do |tabs|
17
+ # @loco_example Radio button tabs with content
18
+ # = daisy_tabs(css: "tabs-lift", radio: true) do |tabs|
19
19
  # - tabs.with_tab(title: "Tab 1", checked: true) do
20
20
  # %p Tab 1 content
21
21
  # - tabs.with_tab(title: "Tab 2") do
22
22
  # %p Tab 2 content
23
23
  #
24
- # @example Tabs with custom titles and content
25
- # = daisy_tabs(css: "tabs-lifted") do |tabs|
24
+ # @loco_example Tabs with custom titles and content
25
+ # = daisy_tabs(css: "tabs-lift") do |tabs|
26
26
  # - tabs.with_tab do |tab|
27
27
  # - tab.with_title do
28
28
  # .flex.gap-2
@@ -31,6 +31,16 @@
31
31
  # - tab.with_custom_content(css: "tab-content p-4") do
32
32
  # %p Welcome home!
33
33
  #
34
+ # @loco_example Tabs with different sizes
35
+ # = daisy_tabs(css: "tabs-border tabs-xl") do |tabs|
36
+ # - tabs.with_tab(title: "Extra Large Tab", active: true)
37
+ # - tabs.with_tab(title: "Another Tab")
38
+ #
39
+ # @loco_example Tabs with different sizes
40
+ # = daisy_tabs(css: "tabs-border tabs-lg") do |tabs|
41
+ # - tabs.with_tab(title: "Large Tab", active: true)
42
+ # - tabs.with_tab(title: "Another Tab")
43
+ #
34
44
  class Daisy::Navigation::TabsComponent < LocoMotion::BaseComponent
35
45
 
36
46
  #
@@ -45,21 +55,21 @@ class Daisy::Navigation::TabsComponent < LocoMotion::BaseComponent
45
55
  # @slot custom_content Custom content to be rendered after the tab. Use this
46
56
  # instead of the block content for complete control over the content's HTML.
47
57
  #
48
- # @example Basic tab with title
58
+ # @loco_example Basic tab with title
49
59
  # = tabs.with_tab(title: "Home")
50
60
  #
51
- # @example Tab with custom title
61
+ # @loco_example Tab with custom title
52
62
  # = tabs.with_tab do |tab|
53
63
  # - tab.with_title do
54
64
  # .flex.gap-2
55
65
  # = hero_icon("home")
56
66
  # Home
57
67
  #
58
- # @example Tab with content
68
+ # @loco_example Tab with content
59
69
  # = tabs.with_tab(title: "Content") do
60
70
  # %p This is the tab's content
61
71
  #
62
- # @example Tab with custom content
72
+ # @loco_example Tab with custom content
63
73
  # = tabs.with_tab do |tab|
64
74
  # - tab.with_custom_content(css: "tab-content p-4") do
65
75
  # %p Custom content with custom wrapper
@@ -94,7 +104,7 @@ class Daisy::Navigation::TabsComponent < LocoMotion::BaseComponent
94
104
  #
95
105
  # @option kws css [String] Additional CSS classes for styling. Common
96
106
  # options include:
97
- # - Size: `tab-lg`, `tab-md`, `tab-sm`, `tab-xs`
107
+ # - Size: `tab-lg`, `tab-md` (default), `tab-sm`, `tab-xs`
98
108
  # - Width: `w-full`, `!w-14`
99
109
  # - Cursor: `cursor-pointer`, `!cursor-auto`
100
110
  #
@@ -112,9 +122,9 @@ class Daisy::Navigation::TabsComponent < LocoMotion::BaseComponent
112
122
 
113
123
  def before_render
114
124
  # Reset the name to the config option or the parent name if available
115
- @name = config_option(:name, loco_parent&.name)
125
+ @name = config_option(:name, loco_parent.name)
116
126
 
117
- if loco_parent&.radio?
127
+ if loco_parent.radio?
118
128
  setup_radio_button
119
129
  else
120
130
  setup_component
@@ -157,7 +167,7 @@ class Daisy::Navigation::TabsComponent < LocoMotion::BaseComponent
157
167
  # custom_content.to_s if custom_content?
158
168
 
159
169
  capture do
160
- if loco_parent&.radio?
170
+ if loco_parent.radio?
161
171
  concat(part(:component))
162
172
  else
163
173
  concat(part(:component) { concat(title? ? title : @simple_title) })
@@ -187,8 +197,8 @@ class Daisy::Navigation::TabsComponent < LocoMotion::BaseComponent
187
197
  #
188
198
  # @option kws css [String] Additional CSS classes for styling. Common
189
199
  # options include:
190
- # - Style: `tabs-bordered`, `tabs-lifted`
191
- # - Size: `tabs-lg`, `tabs-md`, `tabs-sm`, `tabs-xs`
200
+ # - Style: `tabs-border`, `tabs-lift`
201
+ # - Size: `tabs-xl`, `tabs-lg`, `tabs-md` (default), `tabs-sm`, `tabs-xs`
192
202
  # - Width: `w-full`, `w-[500px]`
193
203
  #
194
204
  def initialize(*args, **kws, &block)
@@ -8,19 +8,22 @@
8
8
  # `:where()` pseudo-class to ensure our default classes have the lowest CSS
9
9
  # specificity.
10
10
  #
11
- # @example Basic icon usage
11
+ # @loco_example Basic icon usage
12
12
  # = hero_icon("academic-cap")
13
13
  # = hero_icon(icon: "adjustments-horizontal")
14
14
  # %span.text-blue-500
15
15
  # = hero_icon("archive-box")
16
16
  #
17
- # @example Customized icons
17
+ # @loco_example Customized icons
18
18
  # = hero_icon("no-symbol", css: "size-4 text-red-600")
19
19
  # = hero_icon("arrow-trending-up", css: "size-10 text-green-600")
20
20
  # = hero_icon("exclamation-triangle", css: "size-14 text-yellow-400 animate-pulse")
21
21
  #
22
22
  class Hero::IconComponent < LocoMotion::BaseComponent
23
- prepend LocoMotion::Concerns::TippableComponent
23
+ # Tippable concern provides tooltip functionality.
24
+ include LocoMotion::Concerns::TippableComponent
25
+
26
+ set_component_name :icon
24
27
 
25
28
  # Create a new instance of the IconComponent.
26
29
  #
@@ -41,17 +44,24 @@ class Hero::IconComponent < LocoMotion::BaseComponent
41
44
  # - Color: `text-red-600`, `text-green-600`, `text-yellow-400`
42
45
  # - Animation: `animate-pulse`, `animate-spin`
43
46
  #
47
+ # @option kws tip [String] The tooltip text to display when hovering over
48
+ # the icon.
49
+ #
44
50
  def initialize(*args, **kws, &block)
45
51
  super
46
52
 
47
53
  # Accept either the :icon keyword argument or the first positional argument
48
54
  @icon = config_option(:icon, args[0])
49
55
  @variant = config_option(:variant)
56
+
57
+ @css = config_option(:css, "")
50
58
  end
51
59
 
52
60
  def before_render
61
+ super
62
+
53
63
  add_html(:component, { variant: @variant }) if @variant
54
- add_css(:component, "[:where(&)]:size-5")
64
+ add_css(:component, "where:size-5") unless @css.include?("size-")
55
65
  end
56
66
 
57
67
  #
@@ -62,6 +72,6 @@ class Hero::IconComponent < LocoMotion::BaseComponent
62
72
  # additional whitespace gets added to the output.
63
73
  #
64
74
  def call
65
- heroicon_tag(@icon, **rendered_html(:component))
75
+ heroicon(@icon, **rendered_html(:component))
66
76
  end
67
77
  end
@@ -106,9 +106,14 @@ module Daisy
106
106
  render_daisy_component(Daisy::DataInput::TextAreaComponent, method, **options)
107
107
  end
108
108
 
109
+ # Add the daisy_cally_input method to FormBuilder
110
+ def daisy_cally_input(method, **options, &block)
111
+ render_daisy_component(Daisy::DataInput::CallyInputComponent, method, **options, &block)
112
+ end
113
+
109
114
  # Add the daisy_select method to FormBuilder
110
115
  def daisy_select(method, options: nil, option_groups: nil, placeholder: nil,
111
- options_css: nil, options_html: {}, **args, &block)
116
+ options_css: nil, options_html: {}, **args, &block)
112
117
  # Extract the name from the form builder's object_name and method
113
118
  name = "#{object_name}[#{method}]"
114
119
 
@@ -132,9 +137,31 @@ module Daisy
132
137
  )
133
138
  end
134
139
 
140
+ # Add the daisy_filter method to FormBuilder
141
+ def daisy_filter(method, options: nil, **args, &block)
142
+ # Extract the name from the form builder's object_name and method
143
+ name = "#{object_name}[#{method}]"
144
+
145
+ # Get the current value from the object
146
+ value = object.try(method)
147
+
148
+ # Generate a default ID if not provided
149
+ id = args[:id] || "#{object_name}_#{method}"
150
+
151
+ # Build the component with the extracted form values and any additional options
152
+ @template.daisy_filter(
153
+ name: name,
154
+ id: id,
155
+ value: value,
156
+ options: options,
157
+ **args,
158
+ &block
159
+ )
160
+ end
161
+
135
162
  private
136
163
 
137
- def render_daisy_component(component_class, method, **options)
164
+ def render_daisy_component(component_class, method, **options, &block)
138
165
  # Get the object name from the form builder
139
166
  object_name = @object_name.to_s
140
167
 
@@ -148,7 +175,7 @@ module Daisy
148
175
  options[:value] ||= object.try(method)
149
176
 
150
177
  # Render the component
151
- @template.render component_class.new(**options)
178
+ @template.render(component_class.new(**options), &block)
152
179
  end
153
180
  end
154
181
  end
@@ -0,0 +1,62 @@
1
+ = doc_title(title: "Filters", comp: @comp) do |title|
2
+ :markdown
3
+ The Filter component is a group of radio buttons where choosing one option
4
+ hides the others and shows a reset button.
5
+
6
+ = doc_example(title: "Basic Usage") do |doc|
7
+ - doc.with_description do
8
+ :markdown
9
+ The basic filter component takes an `options` array and uses a div element
10
+ with radio buttons styled as buttons.
11
+
12
+ = daisy_filter(name: "frameworks", options: ["Svelte", "Vue", "React"])
13
+
14
+
15
+ = doc_example(title: "With Hash Options") do |doc|
16
+ - doc.with_description do
17
+ :markdown
18
+ You can also provide options as a hash with values and labels to customize
19
+ the display.
20
+
21
+ :ruby
22
+ options = [
23
+ { label: "Ruby", value: "ruby" },
24
+ { label: "JavaScript", value: "js" },
25
+ { label: "Python", value: "py" }
26
+ ]
27
+
28
+ = daisy_filter(name: "languages", options: options)
29
+
30
+
31
+ = doc_example(title: "Custom Button Styles") do |doc|
32
+ - doc.with_description do
33
+ :markdown
34
+ You can customize the styles of the buttons and reset button using CSS classes.
35
+
36
+ = daisy_filter(name: "priorities", css: "items-center") do |f|
37
+ - f.with_reset_button(css: "btn-accent btn-sm rounded-full")
38
+ - f.with_option(label: "Low", css: "btn-outline btn-success")
39
+ - f.with_option(label: "Medium", css: "btn-outline btn-warning")
40
+ - f.with_option(label: "High", css: "btn-outline btn-error")
41
+
42
+
43
+ = doc_example(title: "Filter Within Form") do |doc|
44
+ - doc.with_description do
45
+ :markdown
46
+ Filters can be used within forms to submit the selected value.
47
+
48
+ = form_with(url: "", method: :get, scope: :search, class: "mb-4") do |form|
49
+ = daisy_filter(name: "categories") do |f|
50
+ - f.with_option(label: "Category 1")
51
+ - f.with_option(label: "Category 2")
52
+ = form.submit "Apply Filters", class: "btn btn-primary mt-4"
53
+
54
+
55
+ = doc_example(title: "Form Builder") do |doc|
56
+ - doc.with_description do
57
+ :markdown
58
+ You can also use the form builder to create a filter.
59
+
60
+ = form_with(url: "", method: :get, scope: :search, class: "mb-4") do |form|
61
+ = form.daisy_filter(:category, options: ["Technology", "Science", "Arts"])
62
+ = form.submit "Apply Filters", class: "btn btn-primary mt-4"
data/lib/hero.rb CHANGED
@@ -1,4 +1,4 @@
1
- require "heroicons-rails"
1
+ require "rails_heroicon"
2
2
 
3
3
  #
4
4
  # Parent module for all Hero components.
@@ -3,13 +3,17 @@ class LocoMotion::BaseComponent < ViewComponent::Base
3
3
  SELF_CLOSING_TAGS = %i[area base br col embed hr img input keygen link meta param source track wbr].freeze
4
4
  EMPTY_PART_IGNORED_TAGS = %i[textarea].freeze
5
5
 
6
- include Heroicons::IconsHelper
6
+ include RailsHeroicon::Helper
7
7
 
8
8
  class_attribute :component_name
9
9
  class_attribute :component_parts, default: { component: {} }
10
10
  class_attribute :valid_modifiers, default: []
11
11
  class_attribute :valid_sizes, default: []
12
12
 
13
+ # Hooks for concerns to register initialization and setup methods
14
+ class_attribute :component_initializers, default: []
15
+ class_attribute :component_setups, default: []
16
+
13
17
  #
14
18
  # Return the current configuration of this component.
15
19
  #
@@ -31,6 +35,24 @@ class LocoMotion::BaseComponent < ViewComponent::Base
31
35
 
32
36
  # Create our config object
33
37
  @config = LocoMotion::ComponentConfig.new(self, **kws, &block)
38
+
39
+ # Run registered initializer hooks from concerns
40
+ self.class.component_initializers.each { |initializer| send(initializer) }
41
+
42
+ # Allow certain components to skip styling if they are being inherited
43
+ @skip_styling = config_option(:skip_styling, false)
44
+
45
+ # Allow manual passing of the loco parent on init if it's not auto-set
46
+ # via slots
47
+ @loco_parent = kws[:loco_parent] if kws.key?(:loco_parent)
48
+ end
49
+
50
+ #
51
+ # Run registered setup hooks from concerns before rendering.
52
+ #
53
+ def before_render
54
+ # Note: ViewComponent::Base does not define before_render, so no super call needed.
55
+ self.class.component_setups.each { |setup| send(setup) }
34
56
  end
35
57
 
36
58
  #
@@ -87,6 +109,26 @@ class LocoMotion::BaseComponent < ViewComponent::Base
87
109
  end
88
110
  end
89
111
 
112
+ #
113
+ # Register an instance method to be called during component initialization.
114
+ #
115
+ # @param method_name [Symbol] The name of the instance method to call.
116
+ #
117
+ def self.register_component_initializer(method_name)
118
+ # Ensure we don't modify the parent class's array directly
119
+ self.component_initializers += [method_name.to_sym]
120
+ end
121
+
122
+ #
123
+ # Register an instance method to be called before component rendering.
124
+ #
125
+ # @param method_name [Symbol] The name of the instance method to call.
126
+ #
127
+ def self.register_component_setup(method_name)
128
+ # Ensure we don't modify the parent class's array directly
129
+ self.component_setups += [method_name.to_sym]
130
+ end
131
+
90
132
  #
91
133
  # Defines a single modifier of this component. Modifiers control certain
92
134
  # rendering aspects of the component.
@@ -373,6 +415,7 @@ class LocoMotion::BaseComponent < ViewComponent::Base
373
415
  "@valid_sizes=#{valid_sizes.inspect}",
374
416
  "@config=#{@config.inspect}",
375
417
  "@component_parts=#{parts.inspect}",
418
+ "@loco_parent=#{loco_parent.inspect}",
376
419
  ].join(" ") + ">"
377
420
  end
378
421
  end
@@ -40,6 +40,7 @@ class LocoMotion::ComponentConfig
40
40
  @parts[:component][:user_tag_name] = kws[:tag_name] if kws[:tag_name]
41
41
  @parts[:component][:user_css].push(kws[:css]) if kws[:css]
42
42
  @parts[:component][:user_html].deep_merge!(kws[:html]) if kws[:html]
43
+ @parts[:component][:user_stimulus_controllers].push(kws[:controller]) if kws[:controller]
43
44
  @parts[:component][:user_stimulus_controllers].push(kws[:controllers]) if kws[:controllers]
44
45
  end
45
46
 
@@ -0,0 +1,134 @@
1
+ require "active_support/concern"
2
+
3
+ module LocoMotion
4
+ module Concerns
5
+ #
6
+ # The IconableComponent concern provides functionality for components that
7
+ # display icons. It supports both left and right icons and allows for
8
+ # customization of their CSS classes and HTML attributes.
9
+ #
10
+ module IconableComponent
11
+ extend ActiveSupport::Concern
12
+
13
+ included do |base|
14
+ base.register_component_initializer(:_initialize_iconable_component)
15
+ base.register_component_setup(:_setup_iconable_component)
16
+ end
17
+
18
+ protected
19
+
20
+ #
21
+ # Initialize icon-related options.
22
+ #
23
+ # @option kws icon [String] The name of Hero icon to render. This is an
24
+ # alias of `left_icon`.
25
+ #
26
+ # @option kws icon_css [String] The CSS classes to apply to the icon. This
27
+ # is an alias of `left_icon_css`.
28
+ #
29
+ # @option kws icon_html [Hash] Additional HTML attributes to apply to the
30
+ # icon. This is an alias of `left_icon_html`.
31
+ #
32
+ # @option kws left_icon [String] The name of Hero icon to render to the
33
+ # left of the content.
34
+ #
35
+ # @option kws left_icon_css [String] The CSS classes to apply to the left
36
+ # icon.
37
+ #
38
+ # @option kws left_icon_html [Hash] Additional HTML attributes to apply to
39
+ # the left icon.
40
+ #
41
+ # @option kws right_icon [String] The name of Hero icon to render to the
42
+ # right of the content.
43
+ #
44
+ # @option kws right_icon_css [String] The CSS classes to apply to the right
45
+ # icon.
46
+ #
47
+ # @option kws right_icon_html [Hash] Additional HTML attributes to apply to
48
+ # the right icon.
49
+ #
50
+ def _initialize_iconable_component
51
+ @icon = config_option(:icon)
52
+ @icon_css = config_option(:icon_css, default_icon_size)
53
+ @icon_options = config_option(:icon_options, {})
54
+ @icon_html = config_option(:icon_html, {})
55
+
56
+ @left_icon = config_option(:left_icon, @icon)
57
+ @left_icon_css = config_option(:left_icon_css, @icon_css)
58
+ @left_icon_options = config_option(:left_icon_options, @icon_html)
59
+ @left_icon_html = config_option(:left_icon_html, @icon_html)
60
+
61
+ @right_icon = config_option(:right_icon)
62
+ @right_icon_css = config_option(:right_icon_css, @icon_css)
63
+ @right_icon_options = config_option(:right_icon_options, {})
64
+ @right_icon_html = config_option(:right_icon_html, @icon_html)
65
+ end
66
+
67
+ #
68
+ # Configure CSS classes for a component with icons.
69
+ # This adds necessary classes for proper icon spacing and alignment.
70
+ #
71
+ def _setup_iconable_component
72
+ if @icon || @left_icon || @right_icon
73
+ add_css(:component, "where:inline-flex where:items-center where:gap-2")
74
+ end
75
+ end
76
+
77
+ def default_icon_size
78
+ "where:size-5"
79
+ end
80
+
81
+ public # Ensure these helper methods remain public
82
+
83
+ #
84
+ # Returns the HTML attributes for the left icon.
85
+ #
86
+ # @return [Hash] HTML attributes for the left icon
87
+ #
88
+ def left_icon_html
89
+ { class: @left_icon_css }.merge(@left_icon_html)
90
+ end
91
+
92
+ #
93
+ # Returns the HTML attributes for the right icon.
94
+ #
95
+ # @return [Hash] HTML attributes for the right icon
96
+ #
97
+ def right_icon_html
98
+ { class: @right_icon_css }.merge(@right_icon_html)
99
+ end
100
+
101
+ #
102
+ # Determines if any icons are present in the component.
103
+ #
104
+ # @return [Boolean] true if any icons are configured, false otherwise
105
+ #
106
+ def has_icons?
107
+ @left_icon.present? || @right_icon.present?
108
+ end
109
+
110
+ #
111
+ # Renders the left icon as a Hero::IconComponent instance.
112
+ #
113
+ # @return [String] The rendered HTML for the icon
114
+ #
115
+ def render_left_icon
116
+ return unless @left_icon.present?
117
+
118
+ hero_icon(@left_icon, css: @left_icon_css, html: @left_icon_html, **@left_icon_options)
119
+ end
120
+ alias_method :render_icon, :render_left_icon
121
+
122
+ #
123
+ # Renders the right icon using a hero icon.
124
+ #
125
+ # @return [String] The rendered HTML for the icon
126
+ #
127
+ def render_right_icon
128
+ return unless @right_icon.present?
129
+
130
+ hero_icon(@right_icon, css: @right_icon_css, html: @right_icon_html, **@right_icon_options)
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LocoMotion
4
+ module Concerns
5
+ #
6
+ # Can be included in relevant components to add labeling functionality.
7
+ # This adds support for start, end, and floating labels that can either be
8
+ # provided as plain text or customized via slots.
9
+ #
10
+ # @loco_example Basic usage with a start label
11
+ # class MyInputComponent < LocoMotion::BaseComponent
12
+ # include LocoMotion::Concerns::LabelableComponent
13
+ # # component implementation ...
14
+ # end
15
+ #
16
+ # = daisy_my_input(name: "username", start: "Username")
17
+ #
18
+ # @loco_example With an end label (useful for checkboxes/radios)
19
+ # = daisy_checkbox(name: "terms", end: "I agree to the terms")
20
+ #
21
+ # @loco_example With a floating label
22
+ # = daisy_text_input(name: "email", floating: "Email Address")
23
+ #
24
+ # @loco_example Using a custom slot for the label
25
+ # = daisy_text_input(name: "password") do |input|
26
+ # - input.with_floating do
27
+ # Password
28
+ # %span.text-red-500 *
29
+ #
30
+ module LabelableComponent
31
+ extend ActiveSupport::Concern
32
+
33
+ #
34
+ # Called when the module is included in a component class.
35
+ # Sets up the necessary parts & slots for custom label content.
36
+ #
37
+ included do
38
+ define_parts :label_wrapper, :start, :end, :floating
39
+
40
+ renders_one :start
41
+ renders_one :end
42
+ renders_one :floating
43
+
44
+ # NOTE: We DO NOT define attr_reader properties here because it can
45
+ # cause confusion / problems with the parts and slots.
46
+ end
47
+
48
+ #
49
+ # Initializes the component and sets up the label options.
50
+ #
51
+ # @param instance_args [Array] Positional arguments passed to the component
52
+ #
53
+ # @param instance_kws [Hash] Keyword arguments passed to the component
54
+ #
55
+ # @option instance_kws [String, nil] :start Text to display in the start
56
+ # label position
57
+ #
58
+ # @option instance_kws [String, nil] :end Text to display in the end
59
+ # label position
60
+ #
61
+ # @option instance_kws [String, nil] :floating Text to display in the
62
+ # floating label position
63
+ #
64
+ # @option instance_kws [String, nil] :placeholder The input's placeholder
65
+ # text. If not provided and `floating_placeholder` is set, it will use
66
+ # that value.
67
+ #
68
+ # @option instance_kws [String, nil] :floating_placeholder Text to use for
69
+ # both the floating label and the input placeholder. This is a
70
+ # convenience option that sets both the `floating` and `placeholder`
71
+ # options to the same value. Both `floating` and `placeholder` take
72
+ # precedence over `floating_placeholder`.
73
+ #
74
+ # @param instance_block [Proc] Block passed to the component for rendering
75
+ # custom content
76
+ #
77
+ def initialize(*instance_args, **instance_kws, &instance_block)
78
+ super(*instance_args, **instance_kws, &instance_block)
79
+
80
+ @floating_placeholder = config_option(:floating_placeholder)
81
+
82
+ @start = config_option(:start)
83
+ @end = config_option(:end)
84
+ @floating = config_option(:floating, @floating_placeholder)
85
+ @placeholder = config_option(:placeholder, @floating_placeholder)
86
+ end
87
+
88
+ #
89
+ # Sets up the tag names for the label parts before rendering the component.
90
+ # This method is called automatically during the component rendering
91
+ # lifecycle.
92
+ #
93
+ # Note that CSS classes for labels must be handled by the implementing
94
+ # component since requirements differ for each type of input component.
95
+ #
96
+ def before_render
97
+ super
98
+
99
+ set_tag_name(:label_wrapper, :label)
100
+ set_tag_name(:start, :span)
101
+ set_tag_name(:end, :span)
102
+ set_tag_name(:floating, :span)
103
+ end
104
+
105
+ #
106
+ # Checks if any type of label is present.
107
+ #
108
+ # @return [Boolean] true if any label is present, false otherwise
109
+ #
110
+ def has_any_label?
111
+ has_start_label? || has_end_label? || has_floating_label?
112
+ end
113
+
114
+ #
115
+ # Checks if a start label is present.
116
+ #
117
+ # @return [Boolean] true if start label is present, false otherwise
118
+ #
119
+ def has_start_label?
120
+ start? || @start || config_option(:start).present?
121
+ end
122
+
123
+ #
124
+ # Checks if an end label is present.
125
+ #
126
+ # @return [Boolean] true if end label is present, false otherwise
127
+ #
128
+ def has_end_label?
129
+ end? || @end || config_option(:end).present?
130
+ end
131
+
132
+ #
133
+ # Checks if a floating label is present.
134
+ #
135
+ # @return [Boolean] true if floating label is present, false otherwise
136
+ #
137
+ def has_floating_label?
138
+ floating? || @floating || config_option(:floating).present?
139
+ end
140
+ end
141
+ end
142
+ end