stimulus_plumbers 0.3.3 → 0.4.2

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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -0
  3. data/README.md +11 -4
  4. data/app/assets/javascripts/stimulus-plumbers/controllers.manifest.json +386 -0
  5. data/app/assets/javascripts/stimulus-plumbers/stimulus-plumbers-controllers.es.js +756 -304
  6. data/app/assets/javascripts/stimulus-plumbers/stimulus-plumbers-controllers.umd.js +1 -1
  7. data/app/assets/stylesheets/stimulus_plumbers/tokens.css +66 -26
  8. data/config/locales/en.yml +31 -0
  9. data/lib/stimulus_plumbers/components/avatar.rb +22 -13
  10. data/lib/stimulus_plumbers/components/button/group.rb +9 -4
  11. data/lib/stimulus_plumbers/components/button/slots.rb +11 -0
  12. data/lib/stimulus_plumbers/components/button.rb +30 -34
  13. data/lib/stimulus_plumbers/components/calendar/turbo/days_of_month.rb +191 -0
  14. data/lib/stimulus_plumbers/components/calendar/turbo/days_of_week.rb +62 -0
  15. data/lib/stimulus_plumbers/components/calendar/turbo/months_of_year.rb +137 -0
  16. data/lib/stimulus_plumbers/components/calendar/turbo/years_of_decade.rb +114 -0
  17. data/lib/stimulus_plumbers/components/calendar/turbo.rb +112 -0
  18. data/lib/stimulus_plumbers/components/calendar.rb +57 -32
  19. data/lib/stimulus_plumbers/components/card/slots.rb +26 -0
  20. data/lib/stimulus_plumbers/components/card.rb +53 -13
  21. data/lib/stimulus_plumbers/components/combobox/builder.rb +45 -0
  22. data/lib/stimulus_plumbers/components/combobox/date/navigation.rb +80 -0
  23. data/lib/stimulus_plumbers/components/combobox/date/navigator.rb +27 -0
  24. data/lib/stimulus_plumbers/components/combobox/date.rb +55 -28
  25. data/lib/stimulus_plumbers/components/combobox/dropdown.rb +27 -24
  26. data/lib/stimulus_plumbers/components/combobox/options/option.rb +1 -1
  27. data/lib/stimulus_plumbers/components/combobox/options/option_group.rb +1 -1
  28. data/lib/stimulus_plumbers/components/combobox/time.rb +83 -78
  29. data/lib/stimulus_plumbers/components/combobox/trigger.rb +17 -12
  30. data/lib/stimulus_plumbers/components/combobox/typeahead.rb +63 -16
  31. data/lib/stimulus_plumbers/components/combobox.rb +58 -38
  32. data/lib/stimulus_plumbers/components/divider.rb +9 -8
  33. data/lib/stimulus_plumbers/components/icon.rb +5 -1
  34. data/lib/stimulus_plumbers/components/input_group.rb +2 -2
  35. data/lib/stimulus_plumbers/components/link/slots.rb +11 -0
  36. data/lib/stimulus_plumbers/components/link.rb +63 -0
  37. data/lib/stimulus_plumbers/components/list/item/slots.rb +13 -0
  38. data/lib/stimulus_plumbers/components/list/item.rb +83 -0
  39. data/lib/stimulus_plumbers/components/list/section.rb +73 -0
  40. data/lib/stimulus_plumbers/components/list.rb +31 -0
  41. data/lib/stimulus_plumbers/components/popover/panel.rb +32 -0
  42. data/lib/stimulus_plumbers/components/popover/trigger.rb +27 -0
  43. data/lib/stimulus_plumbers/components/popover.rb +44 -18
  44. data/lib/stimulus_plumbers/components/timeline/event/slots.rb +18 -0
  45. data/lib/stimulus_plumbers/components/timeline/event.rb +189 -0
  46. data/lib/stimulus_plumbers/components/timeline/group.rb +42 -0
  47. data/lib/stimulus_plumbers/components/timeline.rb +40 -0
  48. data/lib/stimulus_plumbers/engine.rb +1 -0
  49. data/lib/stimulus_plumbers/form/base.rb +103 -0
  50. data/lib/stimulus_plumbers/form/builder.rb +78 -23
  51. data/lib/stimulus_plumbers/form/field.rb +55 -88
  52. data/lib/stimulus_plumbers/form/fields/error.rb +1 -1
  53. data/lib/stimulus_plumbers/form/fields/fieldset.rb +11 -8
  54. data/lib/stimulus_plumbers/form/fields/hint.rb +1 -1
  55. data/lib/stimulus_plumbers/form/fields/inputs/checkbox.rb +115 -0
  56. data/lib/stimulus_plumbers/form/fields/inputs/combobox.rb +24 -0
  57. data/lib/stimulus_plumbers/form/fields/inputs/datetime.rb +52 -58
  58. data/lib/stimulus_plumbers/form/fields/inputs/file.rb +15 -9
  59. data/lib/stimulus_plumbers/form/fields/inputs/password.rb +46 -28
  60. data/lib/stimulus_plumbers/form/fields/inputs/radio.rb +72 -0
  61. data/lib/stimulus_plumbers/form/fields/inputs/search.rb +73 -65
  62. data/lib/stimulus_plumbers/form/fields/inputs/select/grouped.rb +22 -33
  63. data/lib/stimulus_plumbers/form/fields/inputs/select/timezone.rb +3 -46
  64. data/lib/stimulus_plumbers/form/fields/inputs/select/weekday.rb +3 -26
  65. data/lib/stimulus_plumbers/form/fields/inputs/select.rb +64 -61
  66. data/lib/stimulus_plumbers/form/fields/inputs/submit.rb +10 -7
  67. data/lib/stimulus_plumbers/form/fields/inputs/text.rb +34 -22
  68. data/lib/stimulus_plumbers/form/fields/inputs/text_area.rb +15 -9
  69. data/lib/stimulus_plumbers/form/fields/label/floating.rb +41 -0
  70. data/lib/stimulus_plumbers/form/fields/label.rb +9 -3
  71. data/lib/stimulus_plumbers/form/fields/renderer.rb +39 -0
  72. data/lib/stimulus_plumbers/helpers/button_helper.rb +1 -1
  73. data/lib/stimulus_plumbers/helpers/calendar_helper.rb +2 -2
  74. data/lib/stimulus_plumbers/helpers/calendar_turbo_helper.rb +76 -4
  75. data/lib/stimulus_plumbers/helpers/card_helper.rb +1 -11
  76. data/lib/stimulus_plumbers/helpers/combobox_helper.rb +27 -60
  77. data/lib/stimulus_plumbers/helpers/icon_helper.rb +11 -0
  78. data/lib/stimulus_plumbers/helpers/link_helper.rb +11 -0
  79. data/lib/stimulus_plumbers/helpers/list_helper.rb +11 -0
  80. data/lib/stimulus_plumbers/helpers/plumber_helper.rb +3 -6
  81. data/lib/stimulus_plumbers/helpers/timeline_helper.rb +15 -0
  82. data/lib/stimulus_plumbers/helpers.rb +8 -2
  83. data/lib/stimulus_plumbers/logger.rb +4 -3
  84. data/lib/stimulus_plumbers/plumber/base.rb +6 -1
  85. data/lib/stimulus_plumbers/plumber/dispatcher/klass_proxy.rb +4 -3
  86. data/lib/stimulus_plumbers/plumber/dispatcher/method_call.rb +4 -3
  87. data/lib/stimulus_plumbers/plumber/dispatcher.rb +4 -4
  88. data/lib/stimulus_plumbers/plumber/options/aria.rb +17 -0
  89. data/lib/stimulus_plumbers/plumber/options/html.rb +29 -0
  90. data/lib/stimulus_plumbers/plumber/options/stimulus.rb +29 -0
  91. data/lib/stimulus_plumbers/plumber/options/theme.rb +19 -0
  92. data/lib/stimulus_plumbers/plumber/options/token_list.rb +29 -0
  93. data/lib/stimulus_plumbers/plumber/renderer.rb +136 -41
  94. data/lib/stimulus_plumbers/plumber/slots.rb +74 -0
  95. data/lib/stimulus_plumbers/themes/base.rb +6 -7
  96. data/lib/stimulus_plumbers/themes/schema/avatar/ranges.rb +13 -0
  97. data/lib/stimulus_plumbers/themes/schema/button/ranges.rb +16 -0
  98. data/lib/stimulus_plumbers/themes/schema/card/ranges.rb +13 -0
  99. data/lib/stimulus_plumbers/themes/schema/form/checkbox/ranges.rb +16 -0
  100. data/lib/stimulus_plumbers/themes/schema/form/floating/ranges.rb +15 -0
  101. data/lib/stimulus_plumbers/themes/schema/form/radio/ranges.rb +16 -0
  102. data/lib/stimulus_plumbers/themes/schema/form/ranges.rb +1 -2
  103. data/lib/stimulus_plumbers/themes/schema/link/ranges.rb +14 -0
  104. data/lib/stimulus_plumbers/themes/schema/ranges.rb +1 -5
  105. data/lib/stimulus_plumbers/themes/schema.rb +169 -51
  106. data/lib/stimulus_plumbers/version.rb +1 -1
  107. data/lib/stimulus_plumbers.rb +24 -16
  108. metadata +48 -16
  109. data/lib/stimulus_plumbers/components/action_list/item.rb +0 -30
  110. data/lib/stimulus_plumbers/components/action_list/section.rb +0 -28
  111. data/lib/stimulus_plumbers/components/action_list.rb +0 -29
  112. data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_month.rb +0 -149
  113. data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_week.rb +0 -43
  114. data/lib/stimulus_plumbers/components/calendar/month/turbo.rb +0 -59
  115. data/lib/stimulus_plumbers/components/card/section.rb +0 -31
  116. data/lib/stimulus_plumbers/components/combobox/popover.rb +0 -47
  117. data/lib/stimulus_plumbers/components/combobox/time/drum.rb +0 -43
  118. data/lib/stimulus_plumbers/components/date_picker/navigation.rb +0 -41
  119. data/lib/stimulus_plumbers/components/date_picker/navigator.rb +0 -23
  120. data/lib/stimulus_plumbers/components/popover/builder.rb +0 -25
  121. data/lib/stimulus_plumbers/form/fields/inputs/choice.rb +0 -69
  122. data/lib/stimulus_plumbers/helpers/action_list_helper.rb +0 -25
  123. data/lib/stimulus_plumbers/plumber/html_options.rb +0 -52
@@ -5,110 +5,111 @@ module StimulusPlumbers
5
5
  class Combobox
6
6
  class Time < Plumber::Base
7
7
  STIMULUS_CONTROLLER = "combobox-time"
8
-
9
- def self.default_opts
10
- {
11
- popover: { label: "Picker", role: "dialog", tag: :div }
12
- }
8
+ STIMULUS_ACTION = [
9
+ "#{STIMULUS_CONTROLLER}:selected->#{Combobox::STIMULUS_CONTROLLER}#onSelect",
10
+ "#{STIMULUS_CONTROLLER}:selected->#{Components::Popover::STIMULUS_CONTROLLER}#closeOnSelect"
11
+ ].join(" ").freeze
12
+
13
+ module Metadata
14
+ module_function
15
+
16
+ def haspopup = "dialog"
17
+ def popup_id_for(panel_id) = panel_id
18
+ def trigger_icon = "clock"
19
+ def trigger_options = {}
20
+
21
+ def stimulus_data(_panel_id, options)
22
+ {
23
+ input_formatter_format_value: "time",
24
+ input_formatter_options_value: { format: options.fetch(:format, :h12) }.to_json
25
+ }
26
+ end
13
27
  end
14
28
 
15
- def render(...)
16
- render_time(...)
17
- end
29
+ def render(...) = render_time(...)
18
30
 
19
31
  private
20
32
 
21
- def render_time(format: :h12, step: 1, value: nil)
22
- @format = format
23
- @step = [1, step.to_i].max
24
- @time = parse_time(value)
25
-
26
- template.content_tag(
27
- :div,
28
- **merge_html_options(
29
- { classes: theme.resolve(:combobox_time).fetch(:classes, "") },
30
- { data: { controller: STIMULUS_CONTROLLER,
31
- action: "#{STIMULUS_CONTROLLER}:selected->#{Combobox::STIMULUS_CONTROLLER}#onSelect"
32
- }
33
- }
34
- )
35
- ) do
36
- template.safe_join(drums)
37
- end
33
+ def render_time(panel_attrs: {}, format: :h12, step: 1, value: nil, label: nil, labelledby: nil)
34
+ step = [1, step.to_i].max
35
+ time = parse_time(value)
36
+
37
+ attrs = merge_html_options(panel_attrs, theme.resolve(:combobox_time), dialog_attrs(label, labelledby))
38
+ template.content_tag(:div, **attrs) { template.safe_join(drums(format, step, time)) }
38
39
  end
39
40
 
40
- def drums
41
- cols = [hour_drum, minute_drum]
42
- cols << period_drum if @format == :h12
43
- cols
41
+ def dialog_attrs(label, labelledby)
42
+ resolved_label = label || I18n.t("stimulus_plumbers.combobox.time.dialog_label")
43
+ {
44
+ role: "dialog",
45
+ aria: labelled_aria(resolved_label, labelledby: labelledby),
46
+ data: { controller: STIMULUS_CONTROLLER, action: STIMULUS_ACTION }
47
+ }
44
48
  end
45
49
 
46
- def hour_drum
47
- drum.render(
48
- stimulus_controller: STIMULUS_CONTROLLER,
49
- target: "hour",
50
- label: "Hour",
51
- items: hour_items,
52
- selected: current_hour
53
- )
50
+ def drums(format, step, time)
51
+ cols = [hour_drum(format, time), minute_drum(step, time)]
52
+ cols << period_drum(time) if format == :h12
53
+ cols
54
54
  end
55
55
 
56
- def minute_drum
57
- items = (0...60).step(@step).map do |m|
58
- s = m.to_s.rjust(2, "0")
59
- [s, s]
60
- end
61
- selected = @time ? snap_minute(@time.min).to_s.rjust(2, "0") : nil
62
- drum.render(
63
- stimulus_controller: STIMULUS_CONTROLLER,
64
- target: "minute",
65
- label: "Minute",
66
- items: items,
67
- selected: selected
68
- )
56
+ def hour_drum(format, time)
57
+ items = if format == :h12
58
+ (1..12).map { |h| [h.to_s] * 2 }
59
+ else
60
+ (0..23).map { |h| [h.to_s.rjust(2, "0")] * 2 }
61
+ end
62
+ render_drum("hour", t("hour_label"), items, selected_hour(format, time))
69
63
  end
70
64
 
71
- def period_drum
72
- selected = @time && (@time.hour < 12 ? "AM" : "PM")
73
- drum.render(
74
- stimulus_controller: STIMULUS_CONTROLLER,
75
- target: "period",
76
- label: "Period",
77
- items: [%w[AM AM], %w[PM PM]],
78
- selected: selected
79
- )
65
+ def minute_drum(step, time)
66
+ items = (0...60).step(step).map { |m| [m.to_s.rjust(2, "0")] * 2 }
67
+ selected = time ? round_minute(time.min, step).to_s.rjust(2, "0") : nil
68
+ render_drum("minute", t("minute_label"), items, selected)
80
69
  end
81
70
 
82
- def hour_items
83
- if @format == :h12
84
- (1..12).map { |h| [h.to_s, h.to_s] }
85
- else
86
- (0..23).map do |h|
87
- s = h.to_s.rjust(2, "0")
88
- [s, s]
89
- end
90
- end
71
+ def period_drum(time)
72
+ items = [[t("am"), "AM"], [t("pm"), "PM"]]
73
+ selected = time && (time.hour < 12 ? "AM" : "PM")
74
+ render_drum("period", t("period_label"), items, selected)
91
75
  end
92
76
 
93
- def current_hour
94
- return nil unless @time
77
+ def selected_hour(format, time)
78
+ return nil unless time
95
79
 
96
- if @format == :h12
97
- h = @time.hour % 12
80
+ if format == :h12
81
+ h = time.hour % 12
98
82
  (h.zero? ? 12 : h).to_s
99
83
  else
100
- @time.hour.to_s.rjust(2, "0")
84
+ time.hour.to_s.rjust(2, "0")
101
85
  end
102
86
  end
103
87
 
104
- def snap_minute(minute)
105
- return minute if @step == 1
88
+ def round_minute(minute, step)
89
+ return minute if step == 1
106
90
 
107
- ((minute.to_f / @step).round * @step) % 60
91
+ ((minute.to_f / step).round * step) % 60
108
92
  end
109
93
 
110
- def drum
111
- @drum ||= Time::Drum.new(template)
94
+ def render_drum(target, label, items, selected)
95
+ attrs = merge_html_options(
96
+ theme.resolve(:combobox_listbox),
97
+ theme.resolve(:combobox_time_drum, type: target == "period" ? :period : :unit),
98
+ {
99
+ role: "listbox",
100
+ tabindex: "0",
101
+ aria: { label: label },
102
+ data: { "#{STIMULUS_CONTROLLER}_target": target }
103
+ },
104
+ { data: { action: "click->#{STIMULUS_CONTROLLER}#select keydown->#{STIMULUS_CONTROLLER}#onNavigate" } }
105
+ )
106
+ template.content_tag(:ul, **attrs) do
107
+ template.safe_join(
108
+ items.map do |text, value|
109
+ Options::Option.new(template).render(label: text, value: value, selected: value.to_s == selected.to_s)
110
+ end
111
+ )
112
+ end
112
113
  end
113
114
 
114
115
  def parse_time(value)
@@ -118,6 +119,10 @@ module StimulusPlumbers
118
119
  rescue ArgumentError
119
120
  nil
120
121
  end
122
+
123
+ def t(key)
124
+ I18n.t("stimulus_plumbers.combobox.time.#{key}")
125
+ end
121
126
  end
122
127
  end
123
128
  end
@@ -4,6 +4,11 @@ module StimulusPlumbers
4
4
  module Components
5
5
  class Combobox
6
6
  class Trigger < Plumber::Base
7
+ STIMULUS_ACTION = [
8
+ "focus->#{Components::Popover::STIMULUS_CONTROLLER}#open",
9
+ "keydown.esc->#{Components::Popover::STIMULUS_CONTROLLER}#close"
10
+ ].join(" ").freeze
11
+
7
12
  def render(...)
8
13
  render_trigger(...)
9
14
  end
@@ -12,8 +17,7 @@ module StimulusPlumbers
12
17
 
13
18
  def render_trigger(
14
19
  stimulus_controller:,
15
- popover_id:,
16
- haspopup:,
20
+ popover:,
17
21
  readonly: true,
18
22
  aria: {},
19
23
  id: nil,
@@ -24,8 +28,7 @@ module StimulusPlumbers
24
28
  )
25
29
  input_html = render_input(
26
30
  stimulus_controller: stimulus_controller,
27
- popover_id: popover_id,
28
- haspopup: haspopup,
31
+ popover: popover,
29
32
  readonly: readonly,
30
33
  aria: aria,
31
34
  id: id,
@@ -40,8 +43,7 @@ module StimulusPlumbers
40
43
 
41
44
  def render_input(
42
45
  stimulus_controller:,
43
- popover_id:,
44
- haspopup:,
46
+ popover:,
45
47
  readonly:,
46
48
  aria:,
47
49
  id:,
@@ -49,22 +51,21 @@ module StimulusPlumbers
49
51
  **kwargs
50
52
  )
51
53
  stimulus_data = {
54
+ popover_target: popover.dig(:data, :popover_target),
52
55
  "#{stimulus_controller}_target": "trigger",
53
56
  input_formatter_target: "input",
54
- action: "focus->#{stimulus_controller}#open keydown.esc->#{stimulus_controller}#close"
57
+ action: STIMULUS_ACTION
55
58
  }
56
59
 
57
- trigger_aria = { haspopup: haspopup, expanded: "false", controls: popover_id }
58
-
59
60
  template.tag.input(
60
61
  **merge_html_options(
61
- { classes: theme.resolve(:combobox_trigger).fetch(:classes, "") },
62
+ theme.resolve(:combobox_trigger),
62
63
  {
63
64
  id: id,
64
65
  type: "text",
65
66
  readonly: (readonly ? true : nil),
66
67
  role: "combobox",
67
- aria: trigger_aria.deep_merge(aria),
68
+ aria: popover.fetch(:aria, {}).deep_merge(aria),
68
69
  data: stimulus_data
69
70
  },
70
71
  { data: data },
@@ -82,7 +83,11 @@ module StimulusPlumbers
82
83
  end
83
84
 
84
85
  def render_trigger_icon(name)
85
- Icon.new(template).render(name: name, aria: { hidden: "true" })
86
+ Components::Icon.new(template).render(
87
+ name: name,
88
+ classes: theme.resolve(:combobox_trigger_icon).fetch(:classes, ""),
89
+ aria: { hidden: "true" }
90
+ )
86
91
  end
87
92
  end
88
93
  end
@@ -4,33 +4,80 @@ module StimulusPlumbers
4
4
  module Components
5
5
  class Combobox
6
6
  class Typeahead < Plumber::Base
7
- def self.default_opts
8
- Dropdown.default_opts.deep_merge(
9
- trigger: { aria: { autocomplete: "list" }, readonly: false }
7
+ def self.listbox_id_for(panel_id)
8
+ [panel_id, "listbox"].compact.join("_")
9
+ end
10
+
11
+ module Metadata
12
+ module_function
13
+
14
+ def haspopup = "listbox"
15
+ def popup_id_for(panel_id) = Typeahead.listbox_id_for(panel_id)
16
+ def trigger_icon = nil
17
+ def trigger_options = { readonly: false, aria: { autocomplete: "list" } }
18
+
19
+ def stimulus_data(panel_id, _options)
20
+ {
21
+ input_combobox_combobox_dropdown_outlet: "##{panel_id}",
22
+ action: "input->#{Combobox::STIMULUS_CONTROLLER}#onInput"
23
+ }
24
+ end
25
+ end
26
+
27
+ def render(...) = render_typeahead(...)
28
+
29
+ private
30
+
31
+ def render_typeahead(panel_attrs: {}, options: [], value: nil, labelledby: nil, label: nil, url: nil)
32
+ template.content_tag(
33
+ :div,
34
+ template.safe_join([render_listbox(panel_attrs[:id], options, value, labelledby, label), loading, empty]),
35
+ **wrapper_attrs(panel_attrs: panel_attrs, url: url)
10
36
  )
11
37
  end
12
38
 
13
- def render(options: [], value: nil, label: nil, labelledby: nil)
14
- template.safe_join(
15
- [
16
- Dropdown.new(template).render(options: options, value: value, label: label, labelledby: labelledby),
17
- loading,
18
- empty
19
- ]
39
+ def wrapper_attrs(panel_attrs: {}, url: nil)
40
+ merge_html_options(
41
+ panel_attrs,
42
+ {
43
+ data: {
44
+ controller: Dropdown::STIMULUS_CONTROLLER,
45
+ action: Dropdown::STIMULUS_ACTION,
46
+ combobox_dropdown_url_value: url
47
+ }.compact
48
+ }
20
49
  )
21
50
  end
22
51
 
23
- private
52
+ def render_listbox(panel_id, options, value, labelledby, label)
53
+ template.content_tag(
54
+ :ul,
55
+ Options.new(template).render(options, value: value),
56
+ **merge_html_options(
57
+ theme.resolve(:combobox_listbox),
58
+ {
59
+ id: self.class.listbox_id_for(panel_id),
60
+ role: "listbox",
61
+ aria: labelled_aria(label, labelledby: labelledby),
62
+ data: { "#{Dropdown::STIMULUS_CONTROLLER}_target": "listbox" }
63
+ }
64
+ )
65
+ )
66
+ end
24
67
 
25
68
  def loading
26
69
  template.content_tag(
27
70
  :div,
28
71
  **merge_html_options(
29
- { classes: theme.resolve(:combobox_typeahead_loading).fetch(:classes, "") },
30
- { hidden: "", aria: { live: "polite" }, data: { "#{Dropdown::STIMULUS_CONTROLLER}_target": "loading" } }
72
+ theme.resolve(:combobox_typeahead_loading),
73
+ { hidden: "", role: "status", data: { "#{Dropdown::STIMULUS_CONTROLLER}_target": "loading" } }
31
74
  )
32
75
  ) do
33
- Icon.new(template).render(name: "spinner", classes: "size-(--sp-icon-size) animate-spin")
76
+ Components::Icon.new(template).render(
77
+ name: "spinner",
78
+ classes: theme.resolve(:combobox_typeahead_loading_icon).fetch(:classes, ""),
79
+ aria: { hidden: "true" }
80
+ )
34
81
  end
35
82
  end
36
83
 
@@ -38,10 +85,10 @@ module StimulusPlumbers
38
85
  template.content_tag(
39
86
  :div,
40
87
  **merge_html_options(
41
- { classes: theme.resolve(:combobox_typeahead_empty).fetch(:classes, "") },
88
+ theme.resolve(:combobox_typeahead_empty),
42
89
  { hidden: "", role: "status", data: { "#{Dropdown::STIMULUS_CONTROLLER}_target": "empty" } }
43
90
  )
44
- ) { "No results" }
91
+ ) { I18n.t("stimulus_plumbers.combobox.typeahead.empty", default: "No results") }
45
92
  end
46
93
  end
47
94
  end
@@ -7,60 +7,80 @@ module StimulusPlumbers
7
7
  FORMAT_CONTROLLER = "input-formatter"
8
8
  FORMAT_ACTION = "input-combobox:changed->input-formatter#format"
9
9
 
10
- def self.popover_id_for(trigger_id)
11
- [trigger_id, "popover"].compact.join("_")
12
- end
10
+ def render(trigger: {}, input: {}, id: nil, label: nil, close_on_select: nil, **kwargs, &block)
11
+ trigger_opts = trigger.dup
12
+ builder = resolve_builder(&block)
13
+ trigger_id = id || trigger_opts.delete(:id) || template.sp_dom_id
14
+ panel_id = Popover.panel_id_for(trigger_id)
13
15
 
14
- def render(...)
15
- render_combobox(...)
16
+ template.content_tag(:div, **combobox_attrs(input, close_on_select, builder, panel_id, kwargs)) do
17
+ build_popover(trigger_opts, input, builder, trigger_id, panel_id, label)
18
+ end
16
19
  end
17
20
 
18
21
  private
19
22
 
20
- def render_combobox(trigger: {}, input: {}, popover: {}, **kwargs, &block)
21
- popover_id = self.class.popover_id_for(trigger[:id])
22
- initial_value = input[:value]
23
- haspopup = popover.delete(:haspopup) { popover[:role] || "dialog" }
24
- html_options = merge_html_options({ data: build_stimulus_data(initial_value) }, kwargs)
25
-
26
- template.content_tag(:div, **html_options) do
27
- template.safe_join(
28
- [
29
- combobox_trigger(popover_id, trigger, haspopup),
30
- hidden_input(input),
31
- combobox_popover(popover_id, popover, &block)
32
- ]
33
- )
34
- end
23
+ def resolve_builder
24
+ builder = Combobox::Builder.new
25
+ yield builder if block_given?
26
+ builder
35
27
  end
36
28
 
37
- def build_stimulus_data(initial_value)
38
- {
39
- controller: "#{STIMULUS_CONTROLLER} #{FORMAT_CONTROLLER}",
40
- action: FORMAT_ACTION
41
- }.tap do |data|
42
- data[:input_combobox_value_value] = initial_value if initial_value.present?
29
+ def build_popover(trigger, input, builder, trigger_id, panel_id, label)
30
+ metadata = builder.metadata
31
+
32
+ Components::Popover.new(template).build(panel_id: panel_id) do |p|
33
+ p.trigger(haspopup: metadata.haspopup, controls: metadata.popup_id_for(panel_id)) do |attrs|
34
+ build_combobox_trigger(attrs, trigger, input, metadata, trigger_id, label)
35
+ end
36
+ p.build_panel(classes: theme.resolve(:combobox_popover).fetch(:classes, "")) do |panel_attrs|
37
+ builder.render_panel(template, panel_attrs: panel_attrs)
38
+ end
43
39
  end
44
40
  end
45
41
 
46
- def combobox_trigger(popover_id, trigger, haspopup)
47
- Combobox::Trigger.new(template).render(
48
- stimulus_controller: STIMULUS_CONTROLLER,
49
- popover_id: popover_id,
50
- haspopup: haspopup,
51
- **trigger
42
+ def combobox_attrs(input, close_on_select, builder, panel_id, kwargs)
43
+ merge_html_options(
44
+ theme.resolve(:combobox),
45
+ kwargs,
46
+ { data: stimulus_data(input[:value], close_on_select) },
47
+ { data: builder.metadata.stimulus_data(panel_id, builder.options) }
52
48
  )
53
49
  end
54
50
 
55
- def combobox_popover(popover_id, popover, &block)
56
- Combobox::Popover.new(template).render(
57
- stimulus_controller: STIMULUS_CONTROLLER,
58
- id: popover_id,
59
- **popover,
60
- &block
51
+ def build_combobox_trigger(attrs, trigger, input, metadata, trigger_id, label)
52
+ opts = trigger_options(metadata, trigger)
53
+ opts[:aria] = (opts[:aria] || {}).merge(label: label) if label
54
+
55
+ template.safe_join(
56
+ [
57
+ Combobox::Trigger.new(template).render(
58
+ stimulus_controller: STIMULUS_CONTROLLER,
59
+ popover: attrs,
60
+ id: trigger_id,
61
+ **opts
62
+ ),
63
+ hidden_input(input)
64
+ ]
61
65
  )
62
66
  end
63
67
 
68
+ def trigger_options(metadata, trigger)
69
+ defaults = metadata.trigger_options.dup
70
+ defaults[:icon_trailing] = metadata.trigger_icon if metadata.trigger_icon
71
+ defaults.deep_merge(trigger)
72
+ end
73
+
74
+ def stimulus_data(initial_value, close_on_select)
75
+ data = {
76
+ controller: "#{Popover::STIMULUS_CONTROLLER} #{STIMULUS_CONTROLLER} #{FORMAT_CONTROLLER}",
77
+ action: FORMAT_ACTION
78
+ }
79
+ data[:input_combobox_value_value] = initial_value if initial_value.present?
80
+ data[:popover_close_on_select_value] = close_on_select unless close_on_select.nil?
81
+ data
82
+ end
83
+
64
84
  def hidden_input(input)
65
85
  stimulus_data = merge_html_options(
66
86
  { "#{STIMULUS_CONTROLLER}_target": "input" },
@@ -5,13 +5,12 @@ module StimulusPlumbers
5
5
  class Divider < Plumber::Base
6
6
  def render(label = nil, **kwargs)
7
7
  divider_opts = merge_html_options(
8
- { classes: theme.resolve(:divider).fetch(:classes, "") },
8
+ theme.resolve(:divider),
9
9
  kwargs
10
10
  )
11
11
  template.content_tag(:div, role: "separator", **divider_opts) do
12
12
  if label.blank?
13
- hr_classes = theme.resolve(:divider_separator).fetch(:classes, "")
14
- template.tag.hr(class: hr_classes)
13
+ template.tag.hr(**divider_separator_opts)
15
14
  else
16
15
  render_with(label)
17
16
  end
@@ -21,16 +20,18 @@ module StimulusPlumbers
21
20
  private
22
21
 
23
22
  def render_with(label)
24
- hr_classes = theme.resolve(:divider_separator).fetch(:classes, "")
25
- label_classes = theme.resolve(:divider_label).fetch(:classes, "")
26
23
  template.safe_join(
27
24
  [
28
- template.tag.hr(class: hr_classes),
29
- template.content_tag(:span, label, class: label_classes),
30
- template.tag.hr(class: hr_classes)
25
+ template.tag.hr(**divider_separator_opts),
26
+ template.content_tag(:span, label, **merge_html_options(theme.resolve(:divider_label))),
27
+ template.tag.hr(**divider_separator_opts)
31
28
  ]
32
29
  )
33
30
  end
31
+
32
+ def divider_separator_opts
33
+ merge_html_options(theme.resolve(:divider_separator))
34
+ end
34
35
  end
35
36
  end
36
37
  end
@@ -3,9 +3,13 @@
3
3
  module StimulusPlumbers
4
4
  module Components
5
5
  class Icon < Plumber::Base
6
+ def self.icon_name?(value)
7
+ value.is_a?(Symbol) || (value.is_a?(String) && !value.html_safe?)
8
+ end
9
+
6
10
  def render(name:, **kwargs)
7
11
  html_options = merge_html_options(
8
- { classes: theme.resolve(:icon).fetch(:classes, "") },
12
+ theme.resolve(:icon),
9
13
  kwargs
10
14
  )
11
15
 
@@ -3,9 +3,9 @@
3
3
  module StimulusPlumbers
4
4
  module Components
5
5
  class InputGroup < Plumber::Base
6
- def render(leading: nil, trailing: nil, error: false, **kwargs, &block)
6
+ def render(leading: nil, trailing: nil, error: false, floating: nil, **kwargs, &block)
7
7
  html_options = merge_html_options(
8
- theme.resolve(:input_group, error: error),
8
+ theme.resolve(:input_group, error: error, floating: floating),
9
9
  kwargs
10
10
  )
11
11
  template.content_tag(:div, **html_options) do
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Link
6
+ class Slots < Plumber::Slots
7
+ slot :icon_leading, :icon_trailing
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Link < Plumber::Base
6
+ def render(content = nil, url:, icon_leading: nil, icon_trailing: nil, **kwargs, &block)
7
+ icon_trailing ||= "external-link" if kwargs[:target] == "_blank"
8
+
9
+ slots = Link::Slots.new
10
+ slots.with_icon_leading(icon_leading) if icon_leading
11
+ slots.with_icon_trailing(icon_trailing) if icon_trailing
12
+
13
+ render_link(url: url, **kwargs) do
14
+ build_layout(slots) do
15
+ build_content(content, &block)
16
+ end
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def render_link(url:, target: nil, type: :default, variant: :default, **kwargs, &block)
23
+ html_options = merge_html_options(
24
+ theme.resolve(:link, type: type, variant: variant),
25
+ kwargs
26
+ )
27
+ template.content_tag(:a, href: url, target: target, **html_options) do
28
+ template.capture(&block)
29
+ end
30
+ end
31
+
32
+ def build_layout(slots, &block)
33
+ template.safe_join(
34
+ [
35
+ render_icon_slot(slots, :icon_leading),
36
+ template.capture(&block),
37
+ render_icon_slot(slots, :icon_trailing)
38
+ ]
39
+ )
40
+ end
41
+
42
+ def render_icon_slot(slots, name)
43
+ slots.resolve(name) do |value|
44
+ next value unless Components::Icon.icon_name?(value)
45
+
46
+ Components::Icon.new(template).render(
47
+ name: value,
48
+ classes: theme.resolve(:link_icon).fetch(:classes, ""),
49
+ aria: { hidden: "true" }
50
+ )
51
+ end
52
+ end
53
+
54
+ def build_content(content, &block)
55
+ if block_given?
56
+ template.content_tag(:span, template.capture(&block))
57
+ elsif content
58
+ template.content_tag(:span, content)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class List
6
+ class Item
7
+ class Slots < Plumber::Slots
8
+ slot :icon_leading, :title, :description, :icon_trailing
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end