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,82 @@
1
+ #
2
+ # The List component is a vertical layout to display information in rows. It can
3
+ # contain various content types including text, images, and actions arranged in a
4
+ # consistent format.
5
+ #
6
+ # The List component is useful for displaying structured data like user profiles,
7
+ # media libraries, or content collections with a consistent layout.
8
+ #
9
+ # @part component The main list container (ul element)
10
+ # @part header The optional header container for the list
11
+ #
12
+ # @slot item+ {Daisy::DataDisplay::ListItemComponent} Individual list items or
13
+ # rows in the list
14
+ #
15
+ # @loco_example Basic Usage
16
+ # = daisy_list do |list|
17
+ # - list.with_item { "Item 1" }
18
+ # - list.with_item { "Item 2" }
19
+ # - list.with_item { "Item 3" }
20
+ #
21
+ # @loco_example With Header
22
+ # = daisy_list(header: "Featured Items") do |list|
23
+ # - list.with_item { "Featured Item 1" }
24
+ # - list.with_item { "Featured Item 2" }
25
+ # - list.with_item { "Featured Item 3" }
26
+ #
27
+ # @loco_example With Rich Content
28
+ # = daisy_list(header: "Most played songs", css: "bg-base-100 rounded-box shadow-md") do |list|
29
+ # - list.with_item do |item|
30
+ # .flex.items-center.gap-2
31
+ # = image_tag("album_cover.jpg", class: "size-10 rounded-box")
32
+ # .flex.flex-col
33
+ # .font-medium Song Title
34
+ # .text-xs.opacity-60 Artist Name
35
+ # = daisy_button(icon: "play", css: "btn-ghost btn-square")
36
+ #
37
+ # @loco_example With Dividers
38
+ # = daisy_list(css: "divide-y") do |list|
39
+ # - list.with_item { "Item with divider below" }
40
+ # - list.with_item { "Another divided item" }
41
+ # - list.with_item { "Last item" }
42
+ #
43
+ # @!parse class Daisy::DataDisplay::ListComponent < LocoMotion::BaseComponent; end
44
+ class Daisy::DataDisplay::ListComponent < LocoMotion::BaseComponent
45
+ renders_many :items, "Daisy::DataDisplay::ListItemComponent"
46
+ renders_one :header, LocoMotion::BasicComponent.build(tag_name: :li)
47
+
48
+ set_component_name :list
49
+
50
+ # @return [String] Optional header text for the list
51
+ attr_reader :simple_header, :header_css, :header_html
52
+
53
+ #
54
+ # Create a new List component.
55
+ #
56
+ # @param kwargs [Hash] The keyword arguments for the component.
57
+ #
58
+ # @option kwargs [String] :header Optional header text to display at the top
59
+ # of the list.
60
+ #
61
+ # @option kwargs [String] :css Additional CSS classes to apply to the list.
62
+ # Common options include:
63
+ # - `bg-base-100` for background color
64
+ # - `rounded-box` for rounded corners
65
+ # - `shadow-md` for drop shadow
66
+ # - `divide-y` for dividers between items
67
+ #
68
+ def initialize(**kws, &block)
69
+ super
70
+
71
+ @simple_header = config_option(:header)
72
+ @header_css = config_option(:header_css)
73
+ @header_html = config_option(:header_html)
74
+ end
75
+
76
+ def before_render
77
+ add_css(:component, "list")
78
+ set_tag_name(:component, :ul)
79
+
80
+ with_header(tag_name: :li, css: header_css, html: header_html) { simple_header } if simple_header && !header?
81
+ end
82
+ end
@@ -0,0 +1,39 @@
1
+ #
2
+ # The ListItem component represents an individual row within a List component.
3
+ # It provides a consistent layout for displaying content in a list format.
4
+ #
5
+ # @part image Optional image container for the item
6
+ #
7
+ # @loco_example Basic Usage
8
+ # = daisy_list do |list|
9
+ # - list.with_item { "Simple list item" }
10
+ #
11
+ # @loco_example With Image
12
+ # = daisy_list do |list|
13
+ # - list.with_item do
14
+ # = image_tag(src: "profile.jpg", class: "rounded-full size-8")
15
+ # User Profile
16
+ #
17
+ # @!parse class Daisy::DataDisplay::ListItemComponent < LocoMotion::BaseComponent; end
18
+ class Daisy::DataDisplay::ListItemComponent < LocoMotion::BaseComponent
19
+ #
20
+ # Create a new ListItem component.
21
+ #
22
+ # @param kwargs [Hash] The keyword arguments for the component.
23
+ #
24
+ # @option kwargs [String] :css Additional CSS classes to apply to the list item.
25
+ #
26
+ def initialize(**kwargs, &block)
27
+ super
28
+ end
29
+
30
+ # Called before rendering to setup the component CSS and structure
31
+ def before_render
32
+ set_tag_name(:component, :li)
33
+ add_css(:component, "list-row")
34
+ end
35
+
36
+ def call
37
+ part(:component) { content }
38
+ end
39
+ end
@@ -4,17 +4,17 @@
4
4
  = part(:figure) do
5
5
  - if figure?
6
6
  = figure
7
- - if @icon.present?
8
- = heroicon_tag(@icon, class: "inline-block h-8 w-8 stroke-current")
7
+
8
+ = render_icon
9
+
9
10
  - if @src.present?
10
11
  = daisy_avatar(src: @src)
11
-
12
12
 
13
13
  - if title?
14
14
  = title
15
15
  - elsif @simple_title.present?
16
16
  = part(:title) do
17
- = @simple_title
17
+ = @simple_title
18
18
 
19
19
  = part(:value) do
20
20
  = content
@@ -23,5 +23,4 @@
23
23
  = description
24
24
  - elsif @simple_description.present?
25
25
  = part(:description) do
26
- = @simple_description
27
-
26
+ = @simple_description
@@ -3,6 +3,9 @@
3
3
  # description, and figure. It's perfect for dashboards, summaries, or any
4
4
  # situation where you need to highlight important numbers or metrics.
5
5
  #
6
+ # @note Stats have a transparent background by default. Use `bg-base-100` if you
7
+ # need a background color.
8
+ #
6
9
  # Includes the {LocoMotion::Concerns::TippableComponent} module to enable easy
7
10
  # tooltip addition.
8
11
  #
@@ -21,27 +24,29 @@
21
24
  # image via the src option or an icon via the icon option.
22
25
  #
23
26
  # @loco_example Basic Usage
24
- # = daisy_stat(title: "Downloads", value: "31K", css: "bg-base-200")
27
+ # = daisy_stat(title: "Downloads", value: "31K")
25
28
  #
26
29
  # @loco_example With Description
27
- # = daisy_stat(title: "New Users", value: "2.6K", description: "↗︎ 400 (22%)", css: "bg-base-200")
30
+ # = daisy_stat(title: "New Users", value: "2.6K", description: "↗︎ 400 (22%)")
28
31
  #
29
32
  # @loco_example With Icon
30
- # = daisy_stat(title: "Page Views", value: "89,400", icon: "eye", css: "bg-base-200") do |stat|
33
+ # = daisy_stat(title: "Page Views", value: "89,400", icon: "eye") do |stat|
31
34
  # = stat.with_description do
32
35
  # .flex.items-center.gap-1
33
- # = heroicon_tag "arrow-up", class: "size-4 text-success"
36
+ # = heroicon "arrow-up", class: "size-4 text-success"
34
37
  # %span.text-success 14%
35
38
  # from last month
36
39
  #
37
40
  # @loco_example With Custom Figure
38
- # = daisy_stat(title: "Success Rate", value: "98%", css: "bg-base-200") do |stat|
41
+ # = daisy_stat(title: "Success Rate", value: "98%") do |stat|
39
42
  # = stat.with_figure do
40
43
  # .text-success
41
- # = heroicon_tag "check-circle", class: "size-10"
44
+ # = heroicon "check-circle", class: "size-10"
42
45
  #
43
46
  class Daisy::DataDisplay::StatComponent < LocoMotion::BaseComponent
44
- prepend LocoMotion::Concerns::TippableComponent
47
+ include LocoMotion::Concerns::IconableComponent
48
+ include LocoMotion::Concerns::LinkableComponent
49
+ include LocoMotion::Concerns::TippableComponent
45
50
 
46
51
  set_component_name :stat
47
52
 
@@ -76,17 +81,25 @@ class Daisy::DataDisplay::StatComponent < LocoMotion::BaseComponent
76
81
  # @option kws [String] :icon Name of a heroicon to display in the figure
77
82
  # section.
78
83
  #
84
+ # @option kws [String] :tip The tooltip text to display when hovering over
85
+ # the component.
86
+ #
79
87
  def initialize(*args, **kws, &block)
80
88
  super
81
89
 
82
90
  @simple_title = config_option(:title)
83
91
  @simple_description = config_option(:description)
84
92
  @src = config_option(:src)
85
- @icon = config_option(:icon)
86
93
  end
87
94
 
88
95
  def before_render
89
96
  setup_component
97
+
98
+ super
99
+ end
100
+
101
+ def default_icon_size
102
+ "where:size-8"
90
103
  end
91
104
 
92
105
  private
@@ -0,0 +1,47 @@
1
+ #
2
+ # The StatusComponent displays a small icon to visually show the current status of an element,
3
+ # such as online, offline, error, etc. It follows the DaisyUI status component pattern.
4
+ #
5
+ # @loco_example Basic Status
6
+ # = daisy_status()
7
+ #
8
+ # @loco_example Status with Size
9
+ # = daisy_status(css: "status-xs")
10
+ # = daisy_status(css: "status-sm")
11
+ # = daisy_status(css: "status-md")
12
+ # = daisy_status(css: "status-lg")
13
+ # = daisy_status(css: "status-xl")
14
+ #
15
+ # @loco_example Status with Color
16
+ # = daisy_status(css: "status-primary")
17
+ # = daisy_status(css: "status-secondary")
18
+ # = daisy_status(css: "status-accent")
19
+ # = daisy_status(css: "status-info")
20
+ # = daisy_status(css: "status-success")
21
+ # = daisy_status(css: "status-warning")
22
+ # = daisy_status(css: "status-error")
23
+ #
24
+ # @loco_example Status with Accessibility
25
+ # = daisy_status(css: "status-success", html: { aria: { label: "Status: Online" } })
26
+ class Daisy::DataDisplay::StatusComponent < LocoMotion::BaseComponent
27
+ include LocoMotion::Concerns::TippableComponent
28
+
29
+ def initialize(**kws)
30
+ super(**kws)
31
+ end
32
+
33
+ def before_render
34
+ setup_component
35
+ super # Call super after setup
36
+ end
37
+
38
+ def call
39
+ part(:component)
40
+ end
41
+
42
+ private
43
+
44
+ def setup_component
45
+ add_css(:component, "status")
46
+ end
47
+ end
@@ -37,7 +37,7 @@
37
37
  #
38
38
  # - event.with_middle do
39
39
  # .bg-primary.text-primary-content.p-2.rounded-full
40
- # = heroicon_tag "star"
40
+ # = heroicon "star"
41
41
  #
42
42
  # - event.with_end do
43
43
  # %h3.font-bold Milestone Reached
@@ -0,0 +1,14 @@
1
+ = part(:component) do
2
+ = previous_icon if previous_icon?
3
+ = next_icon if next_icon?
4
+
5
+ - if @month_count
6
+ = part(:months) do
7
+ - @month_count.times do |index|
8
+ = render(MonthComponent.new(**month_options(index)))
9
+
10
+ - elsif months?
11
+ = months
12
+
13
+ - else
14
+ = content
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Daisy
4
+ module DataInput
5
+ # The Cally component provides a customizable calendar interface for date selection.
6
+ # It supports both single date and date range selection, with configurable display
7
+ # options including the number of months to show and navigation controls.
8
+ #
9
+ # @part component The root calendar element that contains all other parts.
10
+ # @part months The container element that holds the individual month components.
11
+ #
12
+ # @slot previous_icon [PreviousIcon] The icon used for navigating to the
13
+ # previous month. Defaults to a chevron-left icon.
14
+ # @see #default_previous_icon
15
+ # @slot next_icon [NextIcon] The icon used for navigating to the next month.
16
+ # Defaults to a chevron-right icon.
17
+ # @see #default_next_icon
18
+ # @slot months+ [MonthComponent] The month components to display in the calendar.
19
+ # Multiple months can be displayed side by side.
20
+ # @see #month_options
21
+ #
22
+ # @loco_example Basic calendar with default options
23
+ # = daisy_cally
24
+ #
25
+ # @loco_example Calendar with range selection enabled
26
+ # = daisy_cally(modifier: :range)
27
+ #
28
+ # @loco_example Calendar showing multiple months with custom value
29
+ # = daisy_cally(months: 2, value: Date.today)
30
+ #
31
+ # @loco_example Calendar with min/max date constraints
32
+ # = daisy_cally(min: 1.month.ago, max: 1.month.from_now)
33
+ class CallyComponent < LocoMotion::BaseComponent
34
+ include ViewComponent::SlotableDefault
35
+
36
+ # A component for the previous navigation icon in the calendar header.
37
+ #
38
+ # @note This is used internally by CallyComponent
39
+ class PreviousIcon < Hero::IconComponent
40
+ def before_render
41
+ super
42
+
43
+ add_html(:component, { slot: "previous" })
44
+ end
45
+ end
46
+
47
+ # A component for the next navigation icon in the calendar header.
48
+ #
49
+ # @note This is used internally by CallyComponent
50
+ class NextIcon < Hero::IconComponent
51
+ def before_render
52
+ super
53
+
54
+ add_html(:component, { slot: "next" })
55
+ end
56
+ end
57
+
58
+ # A component representing a single month in the calendar.
59
+ #
60
+ # @note This is used internally by CallyComponent
61
+ class MonthComponent < LocoMotion::BaseComponent
62
+ # @param offset [Integer, nil] The offset of this month from the start date
63
+ def initialize(**kws)
64
+ super
65
+
66
+ @offset = config_option(:offset)
67
+ end
68
+
69
+ def before_render
70
+ super
71
+
72
+ set_tag_name(:component, "calendar-month")
73
+ add_html(:component, { offset: @offset }) if @offset
74
+ end
75
+
76
+ def call
77
+ part(:component)
78
+ end
79
+ end
80
+
81
+ define_modifiers :range
82
+
83
+ renders_one :previous_icon, PreviousIcon
84
+ renders_one :next_icon, NextIcon
85
+ renders_many :months, MonthComponent
86
+
87
+ define_parts :months
88
+
89
+ # Initializes a new CallyComponent.
90
+ #
91
+ # The Cally component provides a customizable calendar interface for date
92
+ # selection. It supports single date selection by default and can be
93
+ # configured for date range selection. The component automatically
94
+ # handles navigation between months and can display multiple months.
95
+ #
96
+ # @param change [String] ID of an input to update with the selected date.
97
+ # Mutually exclusive with `update`.
98
+ # @param update [String] ID of an element to update with the selected
99
+ # date. Mutually exclusive with `change`.
100
+ # @param id [String] The ID of the calendar element
101
+ # @param value [String, Date] The currently selected date or range
102
+ # @param min [String, Date] The minimum selectable date
103
+ # @param max [String, Date] The maximum selectable date
104
+ # @param today [String, Date] The date to consider as 'today'
105
+ # @param months [Integer] Number of months to display (default: 1)
106
+ def initialize(**kws)
107
+ super
108
+
109
+ # If we don't have any modifiers, assume we want a single month for a date select
110
+ @month_count = config_option(:months, modifiers.blank? ? 1 : nil)
111
+ @change = config_option(:change)
112
+ @update = config_option(:update)
113
+
114
+ @id = config_option(:id)
115
+ @value = config_option(:value)
116
+ @min = config_option(:min)
117
+ @max = config_option(:max)
118
+ @today = config_option(:today)
119
+ end
120
+
121
+ # Configures the calendar component before rendering.
122
+ #
123
+ # Sets up the appropriate tag name based on whether range selection is
124
+ # enabled, adds CSS classes, and configures HTML attributes for the
125
+ # calendar component.
126
+ def before_render
127
+ super
128
+
129
+ if modifiers.include?(:range)
130
+ set_tag_name(:component, "calendar-range")
131
+ else
132
+ set_tag_name(:component, "calendar-date")
133
+ end
134
+
135
+ add_css(:component, "cally")
136
+ add_html(:component, { months: @month_count }) if @month_count
137
+
138
+ add_html(:component, {
139
+ id: @id,
140
+ value: @value,
141
+ min: @min,
142
+ max: @max,
143
+ today: @today
144
+ })
145
+
146
+ if @change
147
+ add_html(:component, { onchange: "document.getElementById('#{@change}').value = this.value" })
148
+ end
149
+
150
+ if @update
151
+ add_html(:component, { onchange: "document.getElementById('#{@update}').innerHTML = this.value" })
152
+ end
153
+ end
154
+
155
+ # Generates options for a month component at the given index.
156
+ #
157
+ # @param index [Integer] The 0-based index of the month
158
+ # @return [Hash] Options hash for the month component
159
+ def month_options(index)
160
+ options = {}
161
+
162
+ options[:offset] = index if index > 0
163
+
164
+ options
165
+ end
166
+
167
+ # Provides a default previous icon if none is specified.
168
+ #
169
+ # @return [PreviousIcon] A chevron-left icon
170
+ def default_previous_icon
171
+ PreviousIcon.new(icon: "chevron-left")
172
+ end
173
+
174
+ # Provides a default next icon if none is specified.
175
+ #
176
+ # @return [NextIcon] A chevron-right icon
177
+ def default_next_icon
178
+ NextIcon.new(icon: "chevron-right")
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,5 @@
1
+ = part(:component) do
2
+ = input
3
+
4
+ = part(:popover) do
5
+ = calendar
@@ -0,0 +1,165 @@
1
+ module Daisy
2
+ module DataInput
3
+ # A specialized input component that combines a text input with a calendar
4
+ # picker. The calendar appears in a popover when the input is focused or
5
+ # clicked.
6
+ #
7
+ # @part popover The container for the calendar popover.
8
+ #
9
+ # @slot input The text input component.
10
+ # @slot calendar The calendar component that appears in the popover.
11
+ #
12
+ # @loco_example Basic Usage
13
+ # = daisy_cally_input(name: "event_date", value: Date.today)
14
+ #
15
+ # @loco_example With custom calendar and input components
16
+ # = daisy_cally_input(name: "event_date") do |c|
17
+ # = c.with_calendar(css: "custom-calendar")
18
+ # = c.with_input(css: "custom-input")
19
+ class CallyInputComponent < LocoMotion::BaseComponent
20
+ # A specialized text input component for the CallyInput that handles
21
+ # popover targeting and data attributes needed for the Stimulus
22
+ # controller.
23
+ class CallyTextInputComponent < Daisy::DataInput::TextInputComponent
24
+ # Sets up the HTML attributes needed for the text input before
25
+ # rendering. Configures the popover target, ID, name, and data
26
+ # attributes required for the Stimulus controller to function properly.
27
+ #
28
+ # @return [void]
29
+ def before_render
30
+ parent_config = loco_parent.config
31
+
32
+ # Since we're pulling options from the parent, we have to do a little
33
+ # more work to ensure we set it up correctly.
34
+ @start = config_option(:start, parent_config.options[:start])
35
+ @end = config_option(:end, parent_config.options[:end])
36
+ @floating_placeholder = config_option(:floating_placeholder, parent_config.options[:floating_placeholder])
37
+ @floating = config_option(:floating, parent_config.options[:floating] || @floating_placeholder)
38
+ @placeholder = config_option(:placeholder, parent_config.options[:placeholder] || @floating_placeholder)
39
+
40
+ super
41
+
42
+ add_html(:component, {
43
+ popovertarget: loco_parent.popover_id,
44
+ id: @id || loco_parent.input_id,
45
+ name: @name || loco_parent.name,
46
+ value: @value || parent_config.options[:value],
47
+ placeholder: @placeholder,
48
+ style: "anchor-name:--#{loco_parent.anchor}",
49
+ data: {
50
+ "loco-cally-input-target": "input"
51
+ }
52
+ })
53
+ end
54
+
55
+ def call
56
+ render_parent_to_string
57
+ end
58
+ end
59
+
60
+ # A specialized calendar component for the CallyInput that handles the calendar
61
+ # display and interaction within a popover.
62
+ class CallyCalendarComponent < Daisy::DataInput::CallyComponent
63
+ # Sets up the HTML attributes needed for the calendar before rendering.
64
+ # Configures the ID, value, and data attributes required for the Stimulus controller.
65
+ #
66
+ # @return [void]
67
+ def before_render
68
+ super
69
+
70
+ add_html(:component, {
71
+ id: @id || loco_parent.calendar_id,
72
+ value: @value || loco_parent.value,
73
+ data: {
74
+ "loco-cally-input-target": "calendar"
75
+ }
76
+ })
77
+ end
78
+
79
+ def call
80
+ render_parent_to_string
81
+ end
82
+ end
83
+
84
+ include ViewComponent::SlotableDefault
85
+ include LocoMotion::Concerns::LabelableComponent
86
+
87
+ define_parts :popover
88
+
89
+ renders_one :calendar, CallyCalendarComponent
90
+ renders_one :input, CallyTextInputComponent
91
+
92
+ attr_reader :id, :name, :value, :calendar_id, :input_id, :popover_id, :anchor, :auto_scroll_padding
93
+
94
+ # Initializes a new CallyInputComponent.
95
+ #
96
+ # @param [Hash] kws The options hash
97
+ # @option kws [String] :id A unique identifier for the input (default: auto-generated)
98
+ # @option kws [String] :name The name attribute for the input field
99
+ # @option kws [Date, String] :value The initial value of the input (default: nil)
100
+ # @option kws [Integer] :auto_scroll_padding The padding to use when scrolling the calendar into view (default: 100)
101
+ # @option kws [String] :css Additional CSS classes for the component
102
+ def initialize(**kws)
103
+ super(**kws)
104
+
105
+ @id = config_option(:id, SecureRandom.uuid)
106
+ @name = config_option(:name)
107
+ @value = config_option(:value)
108
+ @auto_scroll_padding = config_option(:auto_scroll_padding, 100)
109
+
110
+ # Input ID should match our ID
111
+ @input_id = @id
112
+
113
+ # Other IDs / options are generated
114
+ @calendar_id = "#{@id}-calendar"
115
+ @popover_id = "#{@id}-popover"
116
+ @anchor = "#{@id}-anchor"
117
+ end
118
+
119
+ # Sets up the component before rendering.
120
+ # Calls the parent's before_render and then runs the component setup.
121
+ #
122
+ # @return [void]
123
+ def before_render
124
+ super
125
+
126
+ setup_component
127
+ end
128
+
129
+ # Provides a default calendar component instance.
130
+ # This is used when no custom calendar component is provided.
131
+ #
132
+ # @return [CallyCalendarComponent] A new instance of the default calendar component
133
+ def default_calendar
134
+ CallyCalendarComponent.new
135
+ end
136
+
137
+ # Provides a default input component instance.
138
+ # This is used when no custom input component is provided.
139
+ #
140
+ # @return [CallyTextInputComponent] A new instance of the default input component
141
+ def default_input
142
+ CallyTextInputComponent.new
143
+ end
144
+
145
+ private
146
+
147
+ # Sets up the component's HTML structure and attributes.
148
+ # Configures the Stimulus controller and popover attributes.
149
+ #
150
+ # @return [void]
151
+ # @private
152
+ def setup_component
153
+ # Ensure we attach the Stimulus controller
154
+ add_stimulus_controller(:component, "loco-cally-input")
155
+
156
+ # Add relevant popover part HTML
157
+ add_html(:popover, { id: @popover_id, popover: "auto", style: "position-anchor:--#{@anchor}" })
158
+ add_html(:popover, { data: { "loco-cally-input-target": "popover", "auto-scroll-padding": @auto_scroll_padding } })
159
+
160
+ # Note that we NEED the dropdown class so that the anchor positioning works properly
161
+ add_css(:popover, "where:dropdown where:bg-base-100 where:rounded where:shadow-lg")
162
+ end
163
+ end
164
+ end
165
+ end