stimulus_plumbers 0.2.8 → 0.2.9

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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/README.md +3 -0
  4. data/app/assets/javascripts/stimulus-plumbers/stimulus-plumbers-controllers.es.js +339 -302
  5. data/app/assets/javascripts/stimulus-plumbers/stimulus-plumbers-controllers.umd.js +1 -1
  6. data/lib/stimulus_plumbers/components/action_list/item.rb +27 -0
  7. data/lib/stimulus_plumbers/components/action_list/section.rb +21 -0
  8. data/lib/stimulus_plumbers/components/action_list.rb +23 -0
  9. data/lib/stimulus_plumbers/components/avatar.rb +72 -0
  10. data/lib/stimulus_plumbers/components/button/group.rb +17 -0
  11. data/lib/stimulus_plumbers/components/button.rb +27 -0
  12. data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_month.rb +2 -2
  13. data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_week.rb +2 -2
  14. data/lib/stimulus_plumbers/components/calendar/month/turbo.rb +55 -0
  15. data/lib/stimulus_plumbers/components/calendar.rb +33 -0
  16. data/lib/stimulus_plumbers/components/card/section.rb +25 -0
  17. data/lib/stimulus_plumbers/components/card.rb +27 -0
  18. data/lib/stimulus_plumbers/components/combobox/autocomplete.rb +30 -34
  19. data/lib/stimulus_plumbers/components/combobox/date.rb +16 -18
  20. data/lib/stimulus_plumbers/components/combobox/dropdown.rb +13 -16
  21. data/lib/stimulus_plumbers/components/combobox/options/option.rb +34 -0
  22. data/lib/stimulus_plumbers/components/combobox/options/option_group.rb +29 -0
  23. data/lib/stimulus_plumbers/components/combobox/options.rb +59 -0
  24. data/lib/stimulus_plumbers/components/combobox/popover.rb +20 -0
  25. data/lib/stimulus_plumbers/components/combobox/time/drum.rb +37 -0
  26. data/lib/stimulus_plumbers/components/combobox/time.rb +32 -15
  27. data/lib/stimulus_plumbers/components/combobox/trigger.rb +38 -0
  28. data/lib/stimulus_plumbers/components/combobox.rb +59 -0
  29. data/lib/stimulus_plumbers/components/date_picker/navigator.rb +1 -1
  30. data/lib/stimulus_plumbers/components/icon.rb +49 -0
  31. data/lib/stimulus_plumbers/components/popover/builder.rb +25 -0
  32. data/lib/stimulus_plumbers/components/popover.rb +26 -0
  33. data/lib/stimulus_plumbers/form/builder.rb +7 -5
  34. data/lib/stimulus_plumbers/form/{field_component.rb → field.rb} +1 -1
  35. data/lib/stimulus_plumbers/form/fields/combobox.rb +1 -1
  36. data/lib/stimulus_plumbers/form/fields/error.rb +14 -0
  37. data/lib/stimulus_plumbers/form/fields/group.rb +14 -0
  38. data/lib/stimulus_plumbers/form/fields/hint.rb +14 -0
  39. data/lib/stimulus_plumbers/form/fields/label.rb +21 -0
  40. data/lib/stimulus_plumbers/form/fields/renderer.rb +16 -20
  41. data/lib/stimulus_plumbers/form/fields/search.rb +23 -9
  42. data/lib/stimulus_plumbers/form/fields/submit.rb +23 -0
  43. data/lib/stimulus_plumbers/helpers/action_list_helper.rb +2 -2
  44. data/lib/stimulus_plumbers/helpers/avatar_helper.rb +2 -2
  45. data/lib/stimulus_plumbers/helpers/button_helper.rb +2 -2
  46. data/lib/stimulus_plumbers/helpers/calendar_helper.rb +1 -1
  47. data/lib/stimulus_plumbers/helpers/calendar_turbo_helper.rb +1 -1
  48. data/lib/stimulus_plumbers/helpers/card_helper.rb +2 -2
  49. data/lib/stimulus_plumbers/helpers/combobox_helper.rb +5 -5
  50. data/lib/stimulus_plumbers/helpers/popover_helper.rb +2 -2
  51. data/lib/stimulus_plumbers/plumber/base.rb +20 -0
  52. data/lib/stimulus_plumbers/plumber/dispatcher.rb +111 -0
  53. data/lib/stimulus_plumbers/plumber/html_options.rb +51 -0
  54. data/lib/stimulus_plumbers/plumber/renderer.rb +89 -0
  55. data/lib/stimulus_plumbers/themes/base.rb +9 -15
  56. data/lib/stimulus_plumbers/themes/schema/ranges.rb +5 -5
  57. data/lib/stimulus_plumbers/themes/schema.rb +97 -0
  58. data/lib/stimulus_plumbers/themes/tailwind/calendar.rb +48 -2
  59. data/lib/stimulus_plumbers/themes/tailwind/combobox.rb +75 -0
  60. data/lib/stimulus_plumbers/themes/tailwind_theme.rb +2 -0
  61. data/lib/stimulus_plumbers/version.rb +1 -1
  62. data/lib/stimulus_plumbers.rb +29 -19
  63. metadata +33 -25
  64. data/lib/stimulus_plumbers/components/action_list/renderer.rb +0 -47
  65. data/lib/stimulus_plumbers/components/avatar/renderer.rb +0 -74
  66. data/lib/stimulus_plumbers/components/button/renderer.rb +0 -33
  67. data/lib/stimulus_plumbers/components/calendar/month/turbo/renderer.rb +0 -57
  68. data/lib/stimulus_plumbers/components/calendar/renderer.rb +0 -35
  69. data/lib/stimulus_plumbers/components/card/renderer.rb +0 -41
  70. data/lib/stimulus_plumbers/components/combobox/option.rb +0 -27
  71. data/lib/stimulus_plumbers/components/combobox/option_group.rb +0 -52
  72. data/lib/stimulus_plumbers/components/combobox/renderer.rb +0 -78
  73. data/lib/stimulus_plumbers/components/icon/renderer.rb +0 -51
  74. data/lib/stimulus_plumbers/components/plumber/base.rb +0 -22
  75. data/lib/stimulus_plumbers/components/plumber/dispatcher.rb +0 -113
  76. data/lib/stimulus_plumbers/components/plumber/html_options.rb +0 -53
  77. data/lib/stimulus_plumbers/components/plumber/renderer.rb +0 -91
  78. data/lib/stimulus_plumbers/components/popover/renderer.rb +0 -46
  79. data/lib/stimulus_plumbers/components/time_picker/renderer.rb +0 -38
  80. data/lib/stimulus_plumbers/themes/base/action_list.rb +0 -14
  81. data/lib/stimulus_plumbers/themes/base/avatar.rb +0 -14
  82. data/lib/stimulus_plumbers/themes/base/button.rb +0 -18
  83. data/lib/stimulus_plumbers/themes/base/calendar.rb +0 -15
  84. data/lib/stimulus_plumbers/themes/base/card.rb +0 -12
  85. data/lib/stimulus_plumbers/themes/base/form.rb +0 -34
  86. data/lib/stimulus_plumbers/themes/base/layout.rb +0 -12
@@ -2,54 +2,50 @@
2
2
 
3
3
  module StimulusPlumbers
4
4
  module Components
5
- module Combobox
6
- # Renders a listbox popover body for autocomplete.
7
- # Re-uses combobox-dropdown for selection/filtering; input-combobox relays
8
- # the trigger input event via the combobox-dropdown outlet.
5
+ class Combobox
9
6
  class Autocomplete < Plumber::Base
10
- include OptionGroup
11
-
12
- DROPDOWN_CONTROLLER = "combobox-dropdown"
13
- DROPDOWN_ACTION = [
14
- "click->combobox-dropdown#select",
15
- "keydown->combobox-dropdown#navigate",
16
- "combobox-dropdown:selected->input-combobox#onSelected"
17
- ].join(" ").freeze
18
-
19
7
  def self.default_opts
20
- {
21
- popover: {
22
- tag: :div,
23
- haspopup: "listbox",
24
- data: { controller: DROPDOWN_CONTROLLER, action: DROPDOWN_ACTION }
25
- },
8
+ Dropdown.default_opts.deep_merge(
26
9
  trigger: { aria_autocomplete: "list", readonly: false }
27
- }
10
+ )
28
11
  end
29
12
 
30
13
  def render(options: [], value: nil, label: nil, **_kwargs)
31
- listbox_attrs = { role: "listbox", data: { "#{DROPDOWN_CONTROLLER}_target": "listbox" } }
32
- listbox_attrs[:aria] = { label: label } if label
14
+ template.safe_join([listbox(options, value, label), loading, empty])
15
+ end
16
+
17
+ private
33
18
 
34
- listbox = template.content_tag(:ul, **listbox_attrs) do
35
- render_items(options, value: value)
19
+ def listbox(options, value, label)
20
+ attrs = merge_html_options(
21
+ { classes: theme.resolve(:combobox_listbox).fetch(:classes, "") },
22
+ { role: "listbox", data: { "#{Dropdown::STIMULUS_CONTROLLER}_target": "listbox" } }
23
+ )
24
+ attrs[:aria] = { label: label } if label
25
+
26
+ template.content_tag(:ul, **attrs) do
27
+ Options.new(template).render(options, value: value)
36
28
  end
29
+ end
37
30
 
38
- loading = template.content_tag(
31
+ def loading
32
+ template.content_tag(
39
33
  :div,
40
- hidden: "",
41
- aria: { live: "polite" },
42
- data: { "#{DROPDOWN_CONTROLLER}_target": "loading" }
34
+ **merge_html_options(
35
+ { classes: theme.resolve(:combobox_autocomplete_loading).fetch(:classes, "") },
36
+ { hidden: "", aria: { live: "polite" }, data: { "#{Dropdown::STIMULUS_CONTROLLER}_target": "loading" } }
37
+ )
43
38
  ) { "" }
39
+ end
44
40
 
45
- empty = template.content_tag(
41
+ def empty
42
+ template.content_tag(
46
43
  :div,
47
- hidden: "",
48
- role: "status",
49
- data: { "#{DROPDOWN_CONTROLLER}_target": "empty" }
44
+ **merge_html_options(
45
+ { classes: theme.resolve(:combobox_autocomplete_empty).fetch(:classes, "") },
46
+ { hidden: "", role: "status", data: { "#{Dropdown::STIMULUS_CONTROLLER}_target": "empty" } }
47
+ )
50
48
  ) { "No results" }
51
-
52
- template.safe_join([listbox, loading, empty])
53
49
  end
54
50
  end
55
51
  end
@@ -2,12 +2,10 @@
2
2
 
3
3
  module StimulusPlumbers
4
4
  module Components
5
- module Combobox
6
- # Renders the date picker popover body: navigation + calendar grid.
7
- # Wires ComboboxDateController; dispatches combobox-date:selected → InputComboboxController.
5
+ class Combobox
8
6
  class Date < Plumber::Base
9
- PICKER_CONTROLLER = "combobox-date"
10
- CALENDAR_OUTLET_KEY = "#{PICKER_CONTROLLER.tr("-", "_")}_calendar_month_outlet".freeze
7
+ STIMULUS_CONTROLLER = "combobox-date"
8
+ CALENDAR_OUTLET = "#{STIMULUS_CONTROLLER}-calendar-month-outlet".freeze
11
9
 
12
10
  def self.default_opts
13
11
  {
@@ -19,17 +17,17 @@ module StimulusPlumbers
19
17
  def render(value: nil, **_kwargs)
20
18
  calendar_id = "combobox_date_#{SecureRandom.hex(8)}_calendar"
21
19
 
22
- template.content_tag(
23
- :div,
24
- data: {
25
- controller: PICKER_CONTROLLER,
26
- CALENDAR_OUTLET_KEY => "##{calendar_id}",
27
- action: [
28
- "calendar-month-observer:selected->#{PICKER_CONTROLLER}#onSelected",
29
- "#{PICKER_CONTROLLER}:selected->input-combobox#onSelected"
30
- ].join(" ")
31
- }.tap { |d| d["#{PICKER_CONTROLLER.tr("-", "_")}_date_value"] = value if value }
32
- ) do
20
+ data = {
21
+ controller: STIMULUS_CONTROLLER,
22
+ CALENDAR_OUTLET => "##{calendar_id}",
23
+ action: [
24
+ "calendar-month-observer:selected->#{STIMULUS_CONTROLLER}#onSelect",
25
+ "#{STIMULUS_CONTROLLER}:selected->#{Combobox::STIMULUS_CONTROLLER}#onSelect"
26
+ ].join(" "),
27
+ "#{STIMULUS_CONTROLLER}-date-value" => value
28
+ }.compact
29
+
30
+ template.content_tag(:div, data: data) do
33
31
  template.safe_join([navigation, calendar_month(id: calendar_id)])
34
32
  end
35
33
  end
@@ -39,12 +37,12 @@ module StimulusPlumbers
39
37
  def navigation
40
38
  DatePicker::Navigation.new(template).render(
41
39
  step: "month",
42
- stimulus_controller: PICKER_CONTROLLER
40
+ stimulus_controller: STIMULUS_CONTROLLER
43
41
  )
44
42
  end
45
43
 
46
44
  def calendar_month(**kwargs)
47
- Calendar::Renderer.new(template).month(**kwargs)
45
+ Calendar.new(template).month(**kwargs)
48
46
  end
49
47
  end
50
48
  end
@@ -2,17 +2,13 @@
2
2
 
3
3
  module StimulusPlumbers
4
4
  module Components
5
- module Combobox
6
- # Renders a static listbox popover body with combobox-dropdown controller.
7
- # Supports flat options, descriptions, and option groups.
5
+ class Combobox
8
6
  class Dropdown < Plumber::Base
9
- include OptionGroup
10
-
11
- DROPDOWN_CONTROLLER = "combobox-dropdown"
12
- DROPDOWN_ACTION = [
13
- "click->combobox-dropdown#select",
14
- "keydown->combobox-dropdown#navigate",
15
- "combobox-dropdown:selected->input-combobox#onSelected"
7
+ STIMULUS_CONTROLLER = "combobox-dropdown"
8
+ STIMULUS_ACTION = [
9
+ "click->#{STIMULUS_CONTROLLER}#select",
10
+ "keydown->#{STIMULUS_CONTROLLER}#onNavigate",
11
+ "#{STIMULUS_CONTROLLER}:selected->#{Combobox::STIMULUS_CONTROLLER}#onSelect"
16
12
  ].join(" ").freeze
17
13
 
18
14
  def self.default_opts
@@ -20,20 +16,21 @@ module StimulusPlumbers
20
16
  popover: {
21
17
  tag: :div,
22
18
  haspopup: "listbox",
23
- data: { controller: DROPDOWN_CONTROLLER, action: DROPDOWN_ACTION }
19
+ data: { controller: STIMULUS_CONTROLLER, action: STIMULUS_ACTION }
24
20
  }
25
21
  }
26
22
  end
27
23
 
28
24
  def render(options: [], value: nil, label: nil, **_kwargs)
29
- listbox_attrs = { role: "listbox", data: { "#{DROPDOWN_CONTROLLER}_target": "listbox" } }
25
+ listbox_attrs = merge_html_options(
26
+ { classes: theme.resolve(:combobox_listbox).fetch(:classes, "") },
27
+ { role: "listbox", data: { "#{STIMULUS_CONTROLLER}_target": "listbox" } }
28
+ )
30
29
  listbox_attrs[:aria] = { label: label } if label
31
30
 
32
- listbox = template.content_tag(:ul, **listbox_attrs) do
33
- render_items(options, value: value)
31
+ template.content_tag(:ul, **listbox_attrs) do
32
+ Options.new(template).render(options, value: value)
34
33
  end
35
-
36
- template.safe_join([listbox])
37
34
  end
38
35
  end
39
36
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Combobox
6
+ class Options
7
+ class Option < Plumber::Base
8
+ def render(label:, value:, description: nil, disabled: false, selected: false)
9
+ aria = { selected: selected ? "true" : "false" }
10
+ aria[:disabled] = "true" if disabled
11
+
12
+ attrs = merge_html_options(
13
+ { classes: theme.resolve(:combobox_option, selected: selected, disabled: disabled).fetch(:classes, "") },
14
+ { role: "option", aria: aria, data: { value: value } }
15
+ )
16
+
17
+ template.content_tag(:li, **attrs) do
18
+ if description
19
+ template.safe_join(
20
+ [
21
+ template.content_tag(:span, label),
22
+ template.content_tag(:span, description)
23
+ ]
24
+ )
25
+ else
26
+ label
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Combobox
6
+ class Options
7
+ class OptionGroup < Plumber::Base
8
+ def render(label:, options:, value: nil)
9
+ attrs = merge_html_options(
10
+ { classes: theme.resolve(:combobox_option_group).fetch(:classes, "") },
11
+ { role: "group", aria: { label: label } }
12
+ )
13
+
14
+ template.content_tag(:li, **attrs) do
15
+ template.safe_join(
16
+ [
17
+ template.content_tag(:span, label, aria: { hidden: "true" }),
18
+ template.content_tag(:ul) do
19
+ Options.new(template).render(options, value: value)
20
+ end
21
+ ]
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Combobox
6
+ class Options < Plumber::Base
7
+ def render(items, value: nil, &block)
8
+ @selected_value = value.to_s
9
+ template.safe_join(items.filter_map { |item| render_item(item, &block) })
10
+ end
11
+
12
+ private
13
+
14
+ def render_item(item, &block)
15
+ attrs = normalize_item(item)
16
+ return nil if attrs.nil?
17
+
18
+ if attrs.key?(:optgroup)
19
+ block ? block.call(attrs) : OptionGroup.new(template).render(**attrs[:optgroup], value: @selected_value)
20
+ else
21
+ block ? block.call(attrs) : Option.new(template).render(**attrs)
22
+ end
23
+ end
24
+
25
+ def normalize_item(item)
26
+ case item
27
+ when Hash then normalize_hash(item)
28
+ when Array then normalize_array(item)
29
+ else
30
+ StimulusPlumbers::Logger.warn("Options#normalize_item: unrecognized item type #{item.class}, skipping")
31
+ nil
32
+ end
33
+ end
34
+
35
+ def normalize_hash(item)
36
+ if item.key?(:options)
37
+ { optgroup: { label: item[:label], options: item[:options] } }
38
+ else
39
+ normalize_option(item[:label], item[:value].to_s, item)
40
+ end
41
+ end
42
+
43
+ def normalize_array(item)
44
+ normalize_option(item[0], item[1].to_s, item[2] || {})
45
+ end
46
+
47
+ def normalize_option(label, value, attrs)
48
+ {
49
+ label: label,
50
+ value: value,
51
+ selected: @selected_value == value,
52
+ disabled: attrs[:disabled] || false,
53
+ description: attrs[:description]
54
+ }
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Combobox
6
+ class Popover < Plumber::Base
7
+ def render(stimulus_controller:, id:, tag: :div, role: nil, label: nil, content: nil, data: {}, **_rest, &block)
8
+ base_data = { "#{stimulus_controller}_target": "popover" }
9
+
10
+ attrs = { id: id, hidden: "", data: merge_data_options(base_data, data.symbolize_keys) }
11
+ attrs[:role] = role if role
12
+ attrs[:aria] = { label: label } if label
13
+
14
+ html_content = block_given? ? template.capture(&block) : content
15
+ template.content_tag(tag, **attrs) { html_content }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Combobox
6
+ class Time
7
+ class Drum < Plumber::Base
8
+ def render(stimulus_controller:, target:, label:, items:, selected: nil)
9
+ template.content_tag(
10
+ :ul,
11
+ **merge_html_options(
12
+ { classes: theme.resolve(:combobox_listbox).fetch(:classes, "") },
13
+ {
14
+ role: "listbox",
15
+ tabindex: "0",
16
+ aria: { label: label },
17
+ data: { "#{stimulus_controller}_target": target }
18
+ },
19
+ { data: { action: "click->#{stimulus_controller}#select keydown->#{stimulus_controller}#onNavigate" } }
20
+ )
21
+ ) do
22
+ template.safe_join(
23
+ items.map do |text, value|
24
+ Options::Option.new(template).render(
25
+ label: text,
26
+ value: value,
27
+ selected: value.to_s == selected.to_s
28
+ )
29
+ end
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -2,11 +2,9 @@
2
2
 
3
3
  module StimulusPlumbers
4
4
  module Components
5
- module Combobox
6
- # Renders an iOS-style drum/wheel time picker as the popover body.
7
- # Wires ComboboxTimeController; dispatches combobox-time:selected → InputComboboxController.
5
+ class Combobox
8
6
  class Time < Plumber::Base
9
- PICKER_CONTROLLER = "combobox-time"
7
+ STIMULUS_CONTROLLER = "combobox-time"
10
8
 
11
9
  def self.default_opts
12
10
  {
@@ -21,10 +19,13 @@ module StimulusPlumbers
21
19
 
22
20
  template.content_tag(
23
21
  :div,
24
- data: {
25
- controller: PICKER_CONTROLLER,
26
- action: "#{PICKER_CONTROLLER}:selected->input-combobox#onSelected"
27
- }
22
+ **merge_html_options(
23
+ { classes: theme.resolve(:combobox_time).fetch(:classes, "") },
24
+ { data: { controller: STIMULUS_CONTROLLER,
25
+ action: "#{STIMULUS_CONTROLLER}:selected->#{Combobox::STIMULUS_CONTROLLER}#onSelect"
26
+ }
27
+ }
28
+ )
28
29
  ) do
29
30
  template.safe_join(drums)
30
31
  end
@@ -39,7 +40,13 @@ module StimulusPlumbers
39
40
  end
40
41
 
41
42
  def hour_drum
42
- drum.render(target: "hour", label: "Hour", items: hour_items, selected: current_hour)
43
+ drum.render(
44
+ stimulus_controller: STIMULUS_CONTROLLER,
45
+ target: "hour",
46
+ label: "Hour",
47
+ items: hour_items,
48
+ selected: current_hour
49
+ )
43
50
  end
44
51
 
45
52
  def minute_drum
@@ -48,14 +55,24 @@ module StimulusPlumbers
48
55
  [s, s]
49
56
  end
50
57
  selected = @time ? snap_minute(@time.min).to_s.rjust(2, "0") : nil
51
- drum.render(target: "minute", label: "Minute", items: items, selected: selected)
58
+ drum.render(
59
+ stimulus_controller: STIMULUS_CONTROLLER,
60
+ target: "minute",
61
+ label: "Minute",
62
+ items: items,
63
+ selected: selected
64
+ )
52
65
  end
53
66
 
54
67
  def period_drum
55
- selected = if @time
56
- @time.hour < 12 ? "AM" : "PM"
57
- end
58
- drum.render(target: "period", label: "Period", items: [%w[AM AM], %w[PM PM]], selected: selected)
68
+ selected = @time && (@time.hour < 12 ? "AM" : "PM")
69
+ drum.render(
70
+ stimulus_controller: STIMULUS_CONTROLLER,
71
+ target: "period",
72
+ label: "Period",
73
+ items: [%w[AM AM], %w[PM PM]],
74
+ selected: selected
75
+ )
59
76
  end
60
77
 
61
78
  def hour_items
@@ -87,7 +104,7 @@ module StimulusPlumbers
87
104
  end
88
105
 
89
106
  def drum
90
- TimePicker::Renderer.new(template)
107
+ @drum ||= Time::Drum.new(template)
91
108
  end
92
109
 
93
110
  def parse_time(value)
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Combobox
6
+ class Trigger < Plumber::Base
7
+ def render(
8
+ stimulus_controller:,
9
+ popover_id:,
10
+ haspopup:,
11
+ readonly: true,
12
+ aria_autocomplete: nil,
13
+ aria_label: nil,
14
+ data: {},
15
+ **_rest
16
+ )
17
+ base_data = {
18
+ "#{stimulus_controller}_target": "trigger",
19
+ input_format_target: "input",
20
+ action: "focus->#{stimulus_controller}#open keydown.esc->#{stimulus_controller}#close"
21
+ }
22
+
23
+ aria = { haspopup: haspopup, expanded: "false", controls: popover_id }
24
+ aria[:autocomplete] = aria_autocomplete if aria_autocomplete
25
+ aria[:label] = aria_label if aria_label
26
+
27
+ template.tag.input(
28
+ type: "text",
29
+ readonly: (readonly ? true : nil),
30
+ role: "combobox",
31
+ aria: aria,
32
+ data: merge_data_options(base_data, data.symbolize_keys)
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Combobox < Plumber::Base
6
+ STIMULUS_CONTROLLER = "input-combobox"
7
+ FORMAT_CONTROLLER = "input-format"
8
+ FORMAT_ACTION = "input-combobox:changed->input-format#format"
9
+
10
+ def render(base_id:, options: {}, **kwargs)
11
+ popover_id = "#{base_id}_popover"
12
+ initial_value = options.dig(:input, :value)
13
+
14
+ base_data = {
15
+ controller: "#{STIMULUS_CONTROLLER} #{FORMAT_CONTROLLER}",
16
+ action: FORMAT_ACTION
17
+ }
18
+ base_data[:input_combobox_value_value] = initial_value if initial_value.present?
19
+
20
+ html_options = merge_html_options({ data: base_data }, kwargs)
21
+
22
+ template.content_tag(:div, **html_options) do
23
+ template.safe_join(
24
+ [
25
+ trigger(popover_id, options),
26
+ hidden_input(options.fetch(:input, {})),
27
+ popover(popover_id, options)
28
+ ]
29
+ )
30
+ end
31
+ end
32
+
33
+ def trigger(popover_id, options)
34
+ haspopup = options.dig(:popover, :haspopup) || options.dig(:popover, :role) || "dialog"
35
+ Combobox::Trigger.new(template).render(
36
+ stimulus_controller: STIMULUS_CONTROLLER,
37
+ popover_id: popover_id,
38
+ haspopup: haspopup,
39
+ **options.fetch(:trigger, {})
40
+ )
41
+ end
42
+
43
+ def popover(popover_id, options)
44
+ Combobox::Popover.new(template).render(
45
+ stimulus_controller: STIMULUS_CONTROLLER,
46
+ id: popover_id,
47
+ **options.fetch(:popover, {})
48
+ )
49
+ end
50
+
51
+ private
52
+
53
+ def hidden_input(opts)
54
+ data = { "#{STIMULUS_CONTROLLER}_target": "value" }.merge(opts.fetch(:data, {}))
55
+ template.tag.input(type: "hidden", name: opts[:name], value: opts[:value], data: data)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -20,7 +20,7 @@ module StimulusPlumbers
20
20
  private
21
21
 
22
22
  def icon(icon_options)
23
- Icon::Renderer.new(template).icon(
23
+ Icon.new(template).render(
24
24
  classes: theme.resolve(:calendar_navigation_navigator_icon).fetch(:classes, ""),
25
25
  **icon_options
26
26
  )
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Icon < Plumber::Base
6
+ ICONS = {
7
+ "arrow-left" => "M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18",
8
+ "arrow-right" => "M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3",
9
+ "arrow-up" => "M4.5 10.5 12 3m0 0 7.5 7.5M12 3v18",
10
+ "arrow-down" => "M19.5 13.5 12 21m0 0-7.5-7.5M12 21V3"
11
+ }.freeze
12
+
13
+ def render(name:, **kwargs)
14
+ html_options = merge_html_options(
15
+ { classes: theme.resolve(:icon).fetch(:classes, "") },
16
+ kwargs
17
+ )
18
+
19
+ if ICONS[name]
20
+ svg_icon(ICONS[name], html_options)
21
+ else
22
+ template.content_tag(:span, nil, **html_options)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def svg_icon(path, html_options)
29
+ template.content_tag(
30
+ :svg,
31
+ xmlns: "http://www.w3.org/2000/svg",
32
+ fill: "none",
33
+ viewBox: "0 0 24 24",
34
+ width: "24",
35
+ height: "24",
36
+ "stroke-width": "1.5",
37
+ stroke: "currentColor",
38
+ **html_options
39
+ ) do
40
+ template.tag.path(
41
+ "stroke-linecap": "round",
42
+ "stroke-linejoin": "round",
43
+ d: path
44
+ )
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Popover
6
+ class Builder
7
+ attr_reader :activator_html, :content_html
8
+
9
+ def initialize(template)
10
+ @template = template
11
+ @activator_html = "".html_safe
12
+ @content_html = "".html_safe
13
+ end
14
+
15
+ def activator(&block)
16
+ @activator_html = @template.capture(&block)
17
+ end
18
+
19
+ def content(&block)
20
+ @content_html = @template.capture(&block)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Popover < Plumber::Base
6
+ def render(interactive: true, **kwargs, &block)
7
+ html_options = merge_html_options(
8
+ { classes: theme.resolve(:popover).fetch(:classes, "") },
9
+ kwargs
10
+ )
11
+
12
+ builder = Popover::Builder.new(template)
13
+ template.capture(builder, &block)
14
+
15
+ template.content_tag(:div, **html_options) do
16
+ wrapped_content = if interactive
17
+ template.content_tag(:template, builder.content_html)
18
+ else
19
+ builder.content_html
20
+ end
21
+ template.safe_join([builder.activator_html, wrapped_content])
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end