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
@@ -0,0 +1,235 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Controller for handling calendar input interactions.
5
+ * Manages the popover calendar UI and synchronization between input and calendar elements.
6
+ */
7
+ export default class extends Controller {
8
+ static targets = ["calendar", "input", "popover"]
9
+
10
+ /**
11
+ * Initializes the controller and sets up event listeners.
12
+ * Binds all methods to ensure proper 'this' context.
13
+ *
14
+ * @returns {void}
15
+ */
16
+ connect() {
17
+
18
+ // Save the bound functions so we can remove them later
19
+ this.boundUpdateInput = this.updateInput.bind(this)
20
+ this.boundUpdateCalendar = this.updateCalendar.bind(this)
21
+ this.boundOpenCalendar = this.openCalendar.bind(this)
22
+ this.boundCloseCalendar = this.closeCalendar.bind(this)
23
+ this.boundHandleKeydown = this.handleKeydown.bind(this)
24
+ this.boundHandleToggle = this.handleToggle.bind(this)
25
+
26
+ // Bind all of our functions
27
+ this.calendarTarget.addEventListener("change", this.boundUpdateInput)
28
+ this.calendarTarget.addEventListener("blur", this.boundCloseCalendar)
29
+
30
+ this.inputTarget.addEventListener("change", this.boundUpdateCalendar)
31
+ this.inputTarget.addEventListener("keyup", this.boundUpdateCalendar)
32
+ this.inputTarget.addEventListener("click", this.boundOpenCalendar)
33
+ this.inputTarget.addEventListener("keydown", this.boundHandleKeydown)
34
+
35
+ this.popoverTarget.addEventListener("toggle", this.boundHandleToggle)
36
+ }
37
+
38
+ /**
39
+ * Cleans up event listeners when the controller is disconnected.
40
+ * Prevents memory leaks by removing all bound event handlers.
41
+ *
42
+ * @returns {void}
43
+ */
44
+ disconnect() {
45
+ this.calendarTarget.removeEventListener("change", this.boundUpdateInput)
46
+ this.calendarTarget.removeEventListener("blur", this.boundCloseCalendar)
47
+
48
+ this.inputTarget.removeEventListener("change", this.boundUpdateCalendar)
49
+ this.inputTarget.removeEventListener("keyup", this.boundUpdateCalendar)
50
+ this.inputTarget.removeEventListener("keydown", this.boundHandleKeydown)
51
+ this.inputTarget.removeEventListener("click", this.boundOpenCalendar)
52
+ this.popoverTarget.removeEventListener("toggle", this.boundHandleToggle)
53
+ }
54
+
55
+ /**
56
+ * Opens the calendar popover.
57
+ *
58
+ * @returns {void}
59
+ */
60
+ openCalendar() {
61
+ // Open the popover
62
+ this.popoverTarget.togglePopover(true)
63
+ }
64
+
65
+ /**
66
+ * Closes the calendar popover if the blur event is not related to calendar elements.
67
+ *
68
+ * @param {FocusEvent} event - The blur event object.
69
+ *
70
+ * @returns {void}
71
+ */
72
+ closeCalendar(event) {
73
+ // Don't close if we're still in the calendar elements
74
+ if (event?.relatedTarget && this.calendarTarget.contains(event.relatedTarget)) {
75
+ return
76
+ }
77
+
78
+ this.popoverTarget.togglePopover(false)
79
+ }
80
+
81
+ /**
82
+ * Opens the calendar if it is closed, or closes it if it is open.
83
+ *
84
+ * @returns {void}
85
+ */
86
+ toggleCalendar() {
87
+ this.popoverTarget.togglePopover()
88
+ }
89
+
90
+ /**
91
+ * Handles the popover toggle event.
92
+ * - When opening: Sets a zero-width space in empty inputs to prevent floating label flicker
93
+ * - When closing: Clears the single-space input to ensure proper floating label behavior
94
+ *
95
+ * @param {Event} event - The toggle event object with newState property
96
+ * @returns {void}
97
+ */
98
+ handleToggle(event) {
99
+ const hasFloatingLabel = this.inputTarget.parentElement.classList.contains("floating-label")
100
+ const isOpen = event.newState == 'open'
101
+
102
+ if (isOpen) {
103
+ // Make sure the calendar is visible
104
+ this.scrollCalendarIntoView()
105
+ }
106
+
107
+ if (hasFloatingLabel) {
108
+ const ZERO_WIDTH_SPACE = '\u200B'
109
+
110
+ if (isOpen) {
111
+ // Set the input to a zero-width space if it is empty to prevent a floating label
112
+ // flicker when the calendar loses focus while setting the date
113
+ if (this.inputTarget.value == null || this.inputTarget.value === '') {
114
+ this.inputTarget.value = ZERO_WIDTH_SPACE;
115
+ }
116
+ } else {
117
+ // Unset the zero-width space input on close so the floating label works again
118
+ if (this.inputTarget.value === ZERO_WIDTH_SPACE) {
119
+ this.inputTarget.value = null
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Ensures the calendar popover is visible within the viewport.
127
+ *
128
+ * If the popover extends beyond the viewport edges, scrolls the window to
129
+ * bring it into view adding any data-auto-scroll-padding.
130
+ *
131
+ * @returns {void}
132
+ */
133
+ scrollCalendarIntoView() {
134
+ const rect = this.popoverTarget.getBoundingClientRect()
135
+ const autoScrollPadding = parseInt(this.popoverTarget.dataset.autoScrollPadding, 10)
136
+ const padding = isNaN(autoScrollPadding) ? 0 : autoScrollPadding
137
+
138
+ if (rect.bottom > window.innerHeight) {
139
+ window.scrollBy({ top: rect.bottom - window.innerHeight + padding, behavior: 'smooth' })
140
+ } else if (rect.top < 0) {
141
+ window.scrollBy({ top: rect.top - padding, behavior: 'smooth' })
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Updates the input value from the calendar and closes the popover.
147
+ * Synchronizes the input field with the selected date.
148
+ *
149
+ * @returns {void}
150
+ */
151
+ updateInput() {
152
+ // Update the input value and close the popover
153
+ this.inputTarget.value = this.calendarTarget.value
154
+ this.closeCalendar()
155
+ }
156
+
157
+ /**
158
+ * Handles keyboard navigation for the calendar input.
159
+ * - SPACE: Toggles the popover
160
+ * - ENTER: Closes an open popover
161
+ * - ARROW DOWN / ARROW UP: Opens the popover and focuses the calendar
162
+ *
163
+ * @param {KeyboardEvent} event - The keyboard event object.
164
+ *
165
+ * @returns {void}
166
+ */
167
+ handleKeydown(event) {
168
+ const hasModifierKeys = event.ctrlKey || event.shiftKey || event.altKey || event.metaKey;
169
+ const isOpen = this.popoverTarget.matches(':popover-open')
170
+
171
+ //
172
+ // SPACE - Toggles the popover.
173
+ //
174
+ if (event.key === ' ' || event.code === 'Space') {
175
+ event.preventDefault()
176
+
177
+ this.toggleCalendar()
178
+ }
179
+
180
+ //
181
+ // ENTER - Closes an open popover
182
+ //
183
+ else if (isOpen && (event.key === 'Enter' || event.code === 'Enter')) {
184
+ // Stop the enter key from triggering the form submission
185
+ event.preventDefault()
186
+
187
+ this.closeCalendar()
188
+ }
189
+
190
+ //
191
+ // ARROW DOWN / ARROW UP - Opens the popover and focuses the calendar
192
+ //
193
+ else if ((event.key === 'ArrowDown' || event.code === 'ArrowUp') && !hasModifierKeys) {
194
+ event.preventDefault()
195
+
196
+ // If the calendar is not already open, open it
197
+ if (!isOpen) {
198
+ this.openCalendar()
199
+ }
200
+
201
+ // Focus the calendar element
202
+ this.calendarTarget.focus()
203
+
204
+ // Forward the keydown event to the calendar
205
+ const forwardedEvent = new KeyboardEvent('keydown', {
206
+ key: event.key,
207
+ code: event.code,
208
+ bubbles: true,
209
+ cancelable: true,
210
+ })
211
+
212
+ // Dispatch the forwarded event to the calendar
213
+ this.calendarTarget.dispatchEvent(forwardedEvent)
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Updates the calendar's value based on the input field.
219
+ * Only updates if the input contains a valid ISO 8601 date.
220
+ *
221
+ * @returns {void}
222
+ */
223
+ updateCalendar() {
224
+ // Only update the calendar if we have a full / valid ISO 8601 date
225
+ let newDate = this.inputTarget.value
226
+
227
+ if (newDate.length !== 10 || new Date(newDate).toString() === "Invalid Date") {
228
+ return
229
+ }
230
+
231
+ // Set the calendar value and focused-date attributes
232
+ this.calendarTarget.value = newDate
233
+ this.calendarTarget.setAttribute('focused-date', newDate)
234
+ }
235
+ }
@@ -0,0 +1,20 @@
1
+ - if has_any_label?
2
+ = part(:label_wrapper) do
3
+ - if has_start_label?
4
+ - if start?
5
+ = start
6
+ - else
7
+ = part(:start) do
8
+ = @start
9
+
10
+ = part(:component)
11
+
12
+ - if has_end_label?
13
+ - if end?
14
+ = self.send(:end)
15
+ - else
16
+ = part(:end) do
17
+ = @end
18
+
19
+ - else
20
+ = part(:component)
@@ -5,6 +5,15 @@
5
5
  # It can be used standalone or with a form builder, and supports various styling
6
6
  # options including toggle mode for switch-like appearance.
7
7
  #
8
+ # @part label_wrapper The wrapper element for labels (when using
9
+ # start/end/floating labels).
10
+ # @part start The element that contains the start label (appears before the
11
+ # checkbox).
12
+ # @part end The element that contains the end label (appears after the checkbox).
13
+ #
14
+ # @slot start Custom content for the start label.
15
+ # @slot end Custom content for the end label.
16
+ #
8
17
  # @loco_example Basic Usage
9
18
  # = daisy_checkbox(name: "accept", id: "accept")
10
19
  #
@@ -17,7 +26,12 @@
17
26
  # @loco_example Disabled Checkbox
18
27
  # = daisy_checkbox(name: "accept", id: "accept", disabled: true)
19
28
  #
29
+ # @loco_example With End Label (common for checkboxes)
30
+ # = daisy_checkbox(name: "terms", id: "terms", end: "I agree to the terms and conditions")
31
+ #
20
32
  class Daisy::DataInput::CheckboxComponent < LocoMotion::BaseComponent
33
+ include LocoMotion::Concerns::LabelableComponent
34
+
21
35
  attr_reader :name, :id, :value, :checked, :toggle, :disabled, :required
22
36
 
23
37
  #
@@ -60,9 +74,16 @@ class Daisy::DataInput::CheckboxComponent < LocoMotion::BaseComponent
60
74
  # Calls the {setup_component} method before rendering the component.
61
75
  #
62
76
  def before_render
77
+ super
78
+
79
+ setup_labels
63
80
  setup_component
64
81
  end
65
82
 
83
+ def setup_labels
84
+ add_css(:label_wrapper, "label") if has_any_label?
85
+ end
86
+
66
87
  #
67
88
  # Sets up the component by configuring the tag name, CSS classes, and HTML
68
89
  # attributes.
@@ -82,11 +103,4 @@ class Daisy::DataInput::CheckboxComponent < LocoMotion::BaseComponent
82
103
  required: @required
83
104
  })
84
105
  end
85
-
86
- #
87
- # Renders the component inline with no additional whitespace.
88
- #
89
- def call
90
- part(:component)
91
- end
92
106
  end
@@ -0,0 +1,8 @@
1
+ = part(:component) do
2
+ - if legend?
3
+ = legend
4
+ - elsif @simple_legend
5
+ = part(:legend) { @simple_legend }
6
+
7
+ -# Render the main content passed to the component block
8
+ = content
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Daisy
4
+ module DataInput
5
+ # Renders a fieldset element, optionally with a legend, to group
6
+ # related form controls or content.
7
+ #
8
+ # @loco_example Basic fieldset
9
+ # = daisy_fieldset do
10
+ # Content inside fieldset
11
+ #
12
+ # @loco_example Fieldset with legend slot
13
+ # = daisy_fieldset do |fieldset|
14
+ # - fieldset.with_legend { "My Legend" }
15
+ #
16
+ # Content inside fieldset
17
+ #
18
+ # @loco_example Fieldset with legend argument
19
+ # = daisy_fieldset(legend: "My Legend") do
20
+ # Content inside fieldset
21
+ #
22
+ class FieldsetComponent < LocoMotion::BaseComponent
23
+ self.component_name = :fieldset
24
+
25
+ define_parts :legend
26
+
27
+ # The legend (title) for the fieldset. Renders a `<legend>` tag.
28
+ # Can be provided as a slot or via the `legend` argument.
29
+ renders_one :legend, LocoMotion::BasicComponent.build(tag_name: :legend, css: "fieldset-legend")
30
+
31
+ # @param legend [String] Optional simple text for the legend.
32
+ # Ignored if the `legend` slot is used.
33
+ def initialize(legend: nil, **kws)
34
+ @simple_legend = legend
35
+
36
+ super(**kws)
37
+ end
38
+
39
+ def before_render
40
+ setup_component
41
+
42
+ super
43
+ end
44
+
45
+ private
46
+
47
+ # Sets up default tags and classes for the component and its parts.
48
+ def setup_component
49
+ set_tag_name(:component, :fieldset)
50
+ set_tag_name(:legend, :legend)
51
+
52
+ add_css(:legend, "fieldset-legend")
53
+ add_css(:component, "fieldset")
54
+ end
55
+ end
56
+ end
57
+ end
@@ -5,6 +5,9 @@
5
5
  # It can be used standalone or with a form builder, and supports
6
6
  # various styling options including different sizes and variants.
7
7
  #
8
+ # @note File inputs have a border by default. Use `file-input-ghost` to remove
9
+ # the border.
10
+ #
8
11
  # @loco_example Basic Usage
9
12
  # = daisy_file_input(name: "document", id: "document")
10
13
  #
@@ -14,6 +17,9 @@
14
17
  # @loco_example With Multiple Files
15
18
  # = daisy_file_input(name: "documents[]", id: "documents", multiple: true)
16
19
  #
20
+ # @loco_example Ghost Style (No Border)
21
+ # = daisy_file_input(name: "document", id: "document", css: "file-input-ghost")
22
+ #
17
23
  # @loco_example Disabled File Input
18
24
  # = daisy_file_input(name: "document", id: "document", disabled: true)
19
25
  #
@@ -0,0 +1,3 @@
1
+ = part(:component) do
2
+ = reset_button if reset_button?
3
+ = render_filter_options
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # The Filter component is a group of radio buttons where choosing one option
5
+ # hides the others and shows a reset button.
6
+ #
7
+ # @loco_example Basic Usage
8
+ # = daisy_filter(name: "frameworks", options: ["Svelte", "Vue", "React"])
9
+ #
10
+ # @loco_example Using Hash Options
11
+ # = daisy_filter(name: "languages", options: [
12
+ # { label: "Ruby", value: "ruby" },
13
+ # { label: "JavaScript", value: "js" },
14
+ # { label: "Python", value: "py" }
15
+ # ])
16
+ #
17
+ # @loco_example Custom Styling
18
+ # = daisy_filter(name: "priorities", css: "items-center") do |f|
19
+ # - f.with_reset_button(css: "btn-accent btn-sm rounded-full")
20
+ # - f.with_option(label: "Low", css: "btn-outline btn-success")
21
+ # - f.with_option(label: "Medium", css: "btn-outline btn-warning")
22
+ # - f.with_option(label: "High", css: "btn-outline btn-error")
23
+ #
24
+ # @loco_example Within Form Builder
25
+ # = form_with(model: @post) do |form|
26
+ # = form.daisy_filter(:category, options: ["News", "Tech", "Sports"])
27
+ # = form.submit "Save", class: "btn btn-primary mt-4"
28
+ #
29
+ # @loco_example Form Builder With Block
30
+ # = form_with(model: @user) do |form|
31
+ # = form.daisy_filter(:role) do |f|
32
+ # - f.with_option(label: "Admin", value: "admin")
33
+ # - f.with_option(label: "Editor", value: "editor")
34
+ # - f.with_option(label: "Viewer", value: "viewer")
35
+ # = form.submit "Update", class: "btn btn-primary mt-4"
36
+ #
37
+ module Daisy
38
+ module DataInput
39
+ class FilterComponent < LocoMotion::BaseComponent
40
+ class FilterOptionComponent < Daisy::DataInput::RadioButtonComponent
41
+ #
42
+ # Initialize a new filter option component.
43
+ #
44
+ # @param kws [Hash] The keyword arguments for the component.
45
+ #
46
+ # @option kws label [String] The aria-label for the radio button.
47
+ #
48
+ def initialize(**kws)
49
+ super(**kws)
50
+
51
+ @label = config_option(:label)
52
+ @index = config_option(:index)
53
+ @skip_styling = true
54
+ end
55
+
56
+ #
57
+ # Setup the component before rendering.
58
+ #
59
+ def before_render
60
+ # Make sure to pull the default name from the parent
61
+ @name = config_option(:name, loco_parent.name)
62
+ @id = config_option(:id, "#{loco_parent.id}_#{@index}")
63
+
64
+ # Call the parent setup first
65
+ super
66
+
67
+ # Add btn class for styling
68
+ add_css(:component, "btn")
69
+ add_html(:component, { id: @id })
70
+
71
+ # Add aria-label if specified
72
+ if @label.present?
73
+ add_html(:component, { "aria-label": @label })
74
+ end
75
+ end
76
+ end
77
+
78
+ class FilterResetComponent < Daisy::DataInput::RadioButtonComponent
79
+ #
80
+ # Initialize a new filter reset component.
81
+ #
82
+ # @param kws [Hash] The keyword arguments for the component.
83
+ #
84
+ # @option kws disabled [Boolean] Whether the reset button is disabled. Defaults to false.
85
+ #
86
+ def initialize(**kws)
87
+ super(**kws)
88
+
89
+ @skip_styling = true
90
+ end
91
+
92
+ #
93
+ # Setup the component before rendering.
94
+ #
95
+ def before_render
96
+ # Make sure to pull the default name from the parent
97
+ @name = config_option(:name, loco_parent.name)
98
+ @id = config_option(:id, "#{loco_parent.id}_reset")
99
+
100
+ # Call parent setup first
101
+ super
102
+
103
+ # Add square styling
104
+ add_css(:component, "where:btn where:btn-square filter-reset")
105
+
106
+ # Set type to reset
107
+ add_html(:component, { type: "radio", id: @id })
108
+ end
109
+ end
110
+
111
+ include ViewComponent::SlotableDefault
112
+
113
+ renders_one :reset_button, FilterResetComponent
114
+ renders_many :options, FilterOptionComponent
115
+
116
+ attr_reader :name, :id
117
+
118
+ #
119
+ # Initialize a new filter component.
120
+ #
121
+ # @param kws [Hash] The keyword arguments for the component.
122
+ #
123
+ # @option kws name [String] Required name attribute for the radio button group.
124
+ #
125
+ # @option kws options [Array] An array of options to display in the filter.
126
+ # Can be an array of strings, symbols, or hashes with :label keys.
127
+ #
128
+ # @option kws value [String] The current value of the filter (for form integration).
129
+ #
130
+ def initialize(**kws)
131
+ super(**kws)
132
+
133
+ @name = config_option(:name)
134
+ @id = config_option(:id, SecureRandom.uuid)
135
+ @options_list = config_option(:options)
136
+ @value = config_option(:value)
137
+ end
138
+
139
+ #
140
+ # Setup the component before rendering.
141
+ #
142
+ def before_render
143
+ super
144
+
145
+ setup_component
146
+ end
147
+
148
+ def default_reset_button
149
+ FilterResetComponent.new(name: @name)
150
+ end
151
+
152
+ #
153
+ # Converts the options array into FilterOptionComponent instances.
154
+ # Handles both hash options (with label keys) and simple string/symbol options.
155
+ #
156
+ # @return [Array<FilterOptionComponent>] Array of option components or empty array if @options_list is nil.
157
+ #
158
+ def standard_options
159
+ return [] unless options_list
160
+
161
+ options_list.map.with_index do |option, index|
162
+ label = option.is_a?(Hash) ? option[:label] : option.to_s
163
+ value = option.is_a?(Hash) ? option[:value] : option
164
+
165
+ # Check if this option should be selected based on the component's value
166
+ checked = @value.present? && @value.to_s == value.to_s
167
+
168
+ Daisy::DataInput::FilterComponent::FilterOptionComponent.new(
169
+ loco_parent: component_ref,
170
+ name: @name,
171
+ label: label,
172
+ value: value,
173
+ checked: checked,
174
+ index: index.to_s # Ensure index is a string
175
+ )
176
+ end
177
+ end
178
+
179
+ #
180
+ # Renders the filter options based on the configuration.
181
+ # This method is used by the template to render options consistently.
182
+ #
183
+ # @return [String] The HTML for all options in the filter.
184
+ #
185
+ def render_filter_options
186
+ result = ""
187
+
188
+ if options?
189
+ options.each do |option|
190
+ result += render(option)
191
+ end
192
+ elsif standard_options.present?
193
+ standard_options.each do |option|
194
+ result += render(option)
195
+ end
196
+ end
197
+
198
+ result.html_safe
199
+ end
200
+
201
+ private
202
+
203
+ #
204
+ # Sets up the component by configuring the tag name, CSS classes, and HTML attributes.
205
+ #
206
+ def setup_component
207
+ # Add base component class
208
+ add_css(:component, "filter")
209
+ end
210
+
211
+ #
212
+ # Ensures the options list is always an array, even if a single option is provided.
213
+ #
214
+ # @return [Array] The list of options as an array.
215
+ #
216
+ def options_list
217
+ [@options_list].flatten.compact
218
+ end
219
+ end
220
+ end
221
+ end
@@ -34,11 +34,11 @@ class Daisy::DataInput::LabelComponent < LocoMotion::BaseComponent
34
34
  # @option kws required [Boolean] Whether the label is for a required input.
35
35
  # Defaults to false.
36
36
  #
37
- def initialize(**kws)
37
+ def initialize(title = nil, **kws)
38
38
  super
39
39
 
40
40
  @for = config_option(:for)
41
- @title = config_option(:title)
41
+ @title = config_option(:title, title)
42
42
  @required = config_option(:required, false)
43
43
  end
44
44
 
@@ -65,7 +65,7 @@ class Daisy::DataInput::RadioButtonComponent < LocoMotion::BaseComponent
65
65
  def setup_component
66
66
  set_tag_name(:component, :input)
67
67
 
68
- add_css(:component, "radio")
68
+ add_css(:component, "radio") unless @skip_styling
69
69
 
70
70
  add_html(:component, {
71
71
  type: "radio",
@@ -3,11 +3,9 @@
3
3
 
4
4
  - if items?
5
5
  - items.each do |item|
6
- - item.set_loco_parent(component_ref)
7
6
  = item
8
7
  - elsif content?
9
8
  = content
10
9
  - elsif star_items.present?
11
10
  - star_items.each do |item|
12
- - item.set_loco_parent(component_ref)
13
11
  = render(item)
@@ -30,7 +30,7 @@ class Daisy::DataInput::RatingComponent < LocoMotion::BaseComponent
30
30
  #
31
31
  def before_render
32
32
  set_tag_name(:component, :input)
33
- add_html(:component, { name: loco_parent&.name, type: "radio" })
33
+ add_html(:component, { name: loco_parent.name, type: "radio" })
34
34
  end
35
35
 
36
36
  #
@@ -120,7 +120,8 @@ class Daisy::DataInput::RatingComponent < LocoMotion::BaseComponent
120
120
  def star_items
121
121
  (1..@max).map do |rating|
122
122
  input_attrs = {
123
- css: ["[:where(&)]:mask [:where(&)]:mask-star", @inputs_css].compact.join(" "),
123
+ loco_parent: component_ref,
124
+ css: ["where:mask where:mask-star", @inputs_css].compact.join(" "),
124
125
  html: {
125
126
  name: @name,
126
127
  value: rating,