stimulus_plumbers 0.3.2 → 0.4.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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -0
  3. data/README.md +3 -1
  4. data/app/assets/javascripts/stimulus-plumbers/controllers.manifest.json +273 -0
  5. data/app/assets/javascripts/stimulus-plumbers/stimulus-plumbers-controllers.es.js +269 -160
  6. data/app/assets/javascripts/stimulus-plumbers/stimulus-plumbers-controllers.umd.js +1 -1
  7. data/app/assets/stylesheets/stimulus_plumbers/tokens.css +56 -13
  8. data/config/locales/en.yml +10 -0
  9. data/lib/stimulus_plumbers/components/avatar.rb +24 -17
  10. data/lib/stimulus_plumbers/components/button/group.rb +15 -4
  11. data/lib/stimulus_plumbers/components/button/slots.rb +11 -0
  12. data/lib/stimulus_plumbers/components/button.rb +45 -11
  13. data/lib/stimulus_plumbers/components/calendar/turbo/days_of_month.rb +151 -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 +99 -0
  16. data/lib/stimulus_plumbers/components/calendar/turbo/years_of_decade.rb +86 -0
  17. data/lib/stimulus_plumbers/components/calendar/turbo.rb +65 -0
  18. data/lib/stimulus_plumbers/components/calendar.rb +70 -26
  19. data/lib/stimulus_plumbers/components/card/slots.rb +26 -0
  20. data/lib/stimulus_plumbers/components/card.rb +56 -10
  21. data/lib/stimulus_plumbers/components/combobox/builder.rb +45 -0
  22. data/lib/stimulus_plumbers/components/combobox/date/navigation.rb +72 -0
  23. data/lib/stimulus_plumbers/components/combobox/date/navigator.rb +25 -0
  24. data/lib/stimulus_plumbers/components/combobox/date.rb +37 -23
  25. data/lib/stimulus_plumbers/components/combobox/dropdown.rb +30 -21
  26. data/lib/stimulus_plumbers/components/combobox/options/option.rb +8 -2
  27. data/lib/stimulus_plumbers/components/combobox/options/option_group.rb +8 -2
  28. data/lib/stimulus_plumbers/components/combobox/options.rb +9 -5
  29. data/lib/stimulus_plumbers/components/combobox/time/drum.rb +8 -2
  30. data/lib/stimulus_plumbers/components/combobox/time.rb +50 -47
  31. data/lib/stimulus_plumbers/components/combobox/trigger.rb +62 -14
  32. data/lib/stimulus_plumbers/components/combobox/typeahead.rb +96 -0
  33. data/lib/stimulus_plumbers/components/combobox.rb +62 -38
  34. data/lib/stimulus_plumbers/components/divider.rb +25 -4
  35. data/lib/stimulus_plumbers/components/icon.rb +11 -17
  36. data/lib/stimulus_plumbers/components/input_group.rb +29 -0
  37. data/lib/stimulus_plumbers/components/link/slots.rb +11 -0
  38. data/lib/stimulus_plumbers/components/link.rb +63 -0
  39. data/lib/stimulus_plumbers/components/list/item/slots.rb +13 -0
  40. data/lib/stimulus_plumbers/components/list/item.rb +83 -0
  41. data/lib/stimulus_plumbers/components/list/section.rb +73 -0
  42. data/lib/stimulus_plumbers/components/list.rb +31 -0
  43. data/lib/stimulus_plumbers/components/popover/panel.rb +32 -0
  44. data/lib/stimulus_plumbers/components/popover/trigger.rb +27 -0
  45. data/lib/stimulus_plumbers/components/popover.rb +44 -14
  46. data/lib/stimulus_plumbers/engine.rb +1 -0
  47. data/lib/stimulus_plumbers/form/base.rb +103 -0
  48. data/lib/stimulus_plumbers/form/builder.rb +71 -24
  49. data/lib/stimulus_plumbers/form/field.rb +56 -88
  50. data/lib/stimulus_plumbers/form/fields/error.rb +1 -1
  51. data/lib/stimulus_plumbers/form/fields/fieldset.rb +11 -8
  52. data/lib/stimulus_plumbers/form/fields/hint.rb +1 -1
  53. data/lib/stimulus_plumbers/form/fields/inputs/checkbox.rb +115 -0
  54. data/lib/stimulus_plumbers/form/fields/inputs/combobox.rb +24 -0
  55. data/lib/stimulus_plumbers/form/fields/inputs/datetime.rb +42 -48
  56. data/lib/stimulus_plumbers/form/fields/inputs/file.rb +9 -8
  57. data/lib/stimulus_plumbers/form/fields/inputs/password.rb +32 -25
  58. data/lib/stimulus_plumbers/form/fields/inputs/radio.rb +60 -0
  59. data/lib/stimulus_plumbers/form/fields/inputs/search.rb +34 -57
  60. data/lib/stimulus_plumbers/form/fields/inputs/select/grouped.rb +22 -29
  61. data/lib/stimulus_plumbers/form/fields/inputs/select/timezone.rb +3 -44
  62. data/lib/stimulus_plumbers/form/fields/inputs/select/weekday.rb +3 -28
  63. data/lib/stimulus_plumbers/form/fields/inputs/select.rb +62 -49
  64. data/lib/stimulus_plumbers/form/fields/inputs/submit.rb +10 -7
  65. data/lib/stimulus_plumbers/form/fields/inputs/text.rb +29 -22
  66. data/lib/stimulus_plumbers/form/fields/inputs/text_area.rb +9 -8
  67. data/lib/stimulus_plumbers/form/fields/label/floating.rb +41 -0
  68. data/lib/stimulus_plumbers/form/fields/label.rb +9 -3
  69. data/lib/stimulus_plumbers/form/fields/renderer.rb +39 -0
  70. data/lib/stimulus_plumbers/helpers/avatar_helper.rb +2 -2
  71. data/lib/stimulus_plumbers/helpers/button_helper.rb +4 -8
  72. data/lib/stimulus_plumbers/helpers/calendar_helper.rb +14 -11
  73. data/lib/stimulus_plumbers/helpers/calendar_turbo_helper.rb +49 -11
  74. data/lib/stimulus_plumbers/helpers/card_helper.rb +2 -12
  75. data/lib/stimulus_plumbers/helpers/combobox_helper.rb +27 -47
  76. data/lib/stimulus_plumbers/helpers/divider_helper.rb +2 -2
  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/popover_helper.rb +2 -2
  82. data/lib/stimulus_plumbers/helpers.rb +6 -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 +20 -23
  96. data/lib/stimulus_plumbers/themes/icons/external.rb +60 -0
  97. data/lib/stimulus_plumbers/themes/icons/registry.rb +36 -0
  98. data/lib/stimulus_plumbers/themes/schema/avatar/ranges.rb +13 -0
  99. data/lib/stimulus_plumbers/themes/schema/button/ranges.rb +16 -0
  100. data/lib/stimulus_plumbers/themes/schema/card/ranges.rb +13 -0
  101. data/lib/stimulus_plumbers/themes/schema/form/checkbox/ranges.rb +16 -0
  102. data/lib/stimulus_plumbers/themes/schema/form/radio/ranges.rb +16 -0
  103. data/lib/stimulus_plumbers/themes/schema/form/ranges.rb +1 -2
  104. data/lib/stimulus_plumbers/themes/schema/icon.rb +57 -15
  105. data/lib/stimulus_plumbers/themes/schema/link/ranges.rb +14 -0
  106. data/lib/stimulus_plumbers/themes/schema/ranges.rb +1 -5
  107. data/lib/stimulus_plumbers/themes/schema.rb +142 -67
  108. data/lib/stimulus_plumbers/version.rb +1 -1
  109. data/lib/stimulus_plumbers.rb +22 -17
  110. metadata +46 -17
  111. data/lib/stimulus_plumbers/components/action_list/item.rb +0 -27
  112. data/lib/stimulus_plumbers/components/action_list/section.rb +0 -22
  113. data/lib/stimulus_plumbers/components/action_list.rb +0 -23
  114. data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_month.rb +0 -145
  115. data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_week.rb +0 -39
  116. data/lib/stimulus_plumbers/components/calendar/month/turbo.rb +0 -55
  117. data/lib/stimulus_plumbers/components/card/section.rb +0 -25
  118. data/lib/stimulus_plumbers/components/combobox/autocomplete.rb +0 -47
  119. data/lib/stimulus_plumbers/components/combobox/popover.rb +0 -24
  120. data/lib/stimulus_plumbers/components/date_picker/navigation.rb +0 -41
  121. data/lib/stimulus_plumbers/components/date_picker/navigator.rb +0 -31
  122. data/lib/stimulus_plumbers/components/popover/builder.rb +0 -25
  123. data/lib/stimulus_plumbers/form/fields/input_group.rb +0 -25
  124. data/lib/stimulus_plumbers/form/fields/inputs/choice.rb +0 -69
  125. data/lib/stimulus_plumbers/helpers/action_list_helper.rb +0 -25
  126. data/lib/stimulus_plumbers/plumber/html_options.rb +0 -52
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Combobox
6
+ class Typeahead < Plumber::Base
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)
36
+ )
37
+ end
38
+
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
+ }
49
+ )
50
+ end
51
+
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
67
+
68
+ def loading
69
+ template.content_tag(
70
+ :div,
71
+ **merge_html_options(
72
+ theme.resolve(:combobox_typeahead_loading),
73
+ { hidden: "", role: "status", data: { "#{Dropdown::STIMULUS_CONTROLLER}_target": "loading" } }
74
+ )
75
+ ) do
76
+ Components::Icon.new(template).render(
77
+ name: "spinner",
78
+ classes: theme.resolve(:combobox_typeahead_loading_icon).fetch(:classes, ""),
79
+ aria: { hidden: "true" }
80
+ )
81
+ end
82
+ end
83
+
84
+ def empty
85
+ template.content_tag(
86
+ :div,
87
+ **merge_html_options(
88
+ theme.resolve(:combobox_typeahead_empty),
89
+ { hidden: "", role: "status", data: { "#{Dropdown::STIMULUS_CONTROLLER}_target": "empty" } }
90
+ )
91
+ ) { I18n.t("stimulus_plumbers.combobox.typeahead.empty", default: "No results") }
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -4,62 +4,86 @@ module StimulusPlumbers
4
4
  module Components
5
5
  class Combobox < Plumber::Base
6
6
  STIMULUS_CONTROLLER = "input-combobox"
7
- FORMAT_CONTROLLER = "input-format"
8
- FORMAT_ACTION = "input-combobox:changed->input-format#format"
7
+ FORMAT_CONTROLLER = "input-formatter"
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
13
-
14
- def render(trigger: {}, input: {}, popover: {}, **kwargs, &block)
15
- popover_id = self.class.popover_id_for(trigger[:id])
16
- initial_value = input[:value]
17
- haspopup = popover.delete(:haspopup) { popover[:role] || "dialog" }
18
- html_options = merge_html_options({ data: build_stimulus_data(initial_value) }, kwargs)
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)
19
15
 
20
- template.content_tag(:div, **html_options) do
21
- template.safe_join(
22
- [
23
- combobox_trigger(popover_id, trigger, haspopup),
24
- hidden_input(input),
25
- combobox_popover(popover_id, popover, &block)
26
- ]
27
- )
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)
28
18
  end
29
19
  end
30
20
 
31
21
  private
32
22
 
33
- def build_stimulus_data(initial_value)
34
- {
35
- controller: "#{STIMULUS_CONTROLLER} #{FORMAT_CONTROLLER}",
36
- action: FORMAT_ACTION
37
- }.tap do |data|
38
- data[:input_combobox_value_value] = initial_value if initial_value.present?
23
+ def resolve_builder
24
+ builder = Combobox::Builder.new
25
+ yield builder if block_given?
26
+ builder
27
+ end
28
+
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
39
39
  end
40
40
  end
41
41
 
42
- def combobox_trigger(popover_id, trigger, haspopup)
43
- Combobox::Trigger.new(template).render(
44
- stimulus_controller: STIMULUS_CONTROLLER,
45
- popover_id: popover_id,
46
- haspopup: haspopup,
47
- **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) }
48
48
  )
49
49
  end
50
50
 
51
- def combobox_popover(popover_id, popover, &block)
52
- Combobox::Popover.new(template).render(
53
- stimulus_controller: STIMULUS_CONTROLLER,
54
- id: popover_id,
55
- **popover,
56
- &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
+ ]
57
65
  )
58
66
  end
59
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
+
60
84
  def hidden_input(input)
61
85
  stimulus_data = merge_html_options(
62
- { "#{STIMULUS_CONTROLLER}_target": "value" },
86
+ { "#{STIMULUS_CONTROLLER}_target": "input" },
63
87
  input.fetch(:data, {})
64
88
  )
65
89
  template.tag.input(type: "hidden", name: input[:name], value: input[:value], data: stimulus_data)
@@ -3,13 +3,34 @@
3
3
  module StimulusPlumbers
4
4
  module Components
5
5
  class Divider < Plumber::Base
6
- def render(**kwargs)
7
- html_options = merge_html_options(
8
- { classes: theme.resolve(:divider).fetch(:classes, "") },
6
+ def render(label = nil, **kwargs)
7
+ divider_opts = merge_html_options(
8
+ theme.resolve(:divider),
9
9
  kwargs
10
10
  )
11
+ template.content_tag(:div, role: "separator", **divider_opts) do
12
+ if label.blank?
13
+ template.tag.hr(**divider_separator_opts)
14
+ else
15
+ render_with(label)
16
+ end
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def render_with(label)
23
+ template.safe_join(
24
+ [
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)
28
+ ]
29
+ )
30
+ end
11
31
 
12
- template.tag.hr(**html_options)
32
+ def divider_separator_opts
33
+ merge_html_options(theme.resolve(:divider_separator))
13
34
  end
14
35
  end
15
36
  end
@@ -3,13 +3,17 @@
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
 
12
- icon_data = Themes::Schema::Icon.resolve(theme.icons[name])
16
+ icon_data = Themes::Schema::Icon.resolve(theme.icons[name.to_s])
13
17
  if icon_data
14
18
  svg_icon(icon_data, html_options)
15
19
  else
@@ -20,21 +24,11 @@ module StimulusPlumbers
20
24
  private
21
25
 
22
26
  def svg_icon(icon_data, html_options)
23
- template.content_tag(
24
- :svg,
25
- xmlns: "http://www.w3.org/2000/svg",
26
- fill: icon_data[:fill],
27
- viewBox: icon_data[:view_box],
28
- width: icon_data[:width],
29
- height: icon_data[:height],
30
- stroke: icon_data[:stroke],
31
- "stroke-width": icon_data[:stroke_width],
32
- **html_options
33
- ) do
34
- template.tag.path(
35
- "stroke-linecap": icon_data[:stroke_linecap],
36
- "stroke-linejoin": icon_data[:stroke_linejoin],
37
- d: icon_data[:d]
27
+ template.content_tag(:svg, nil, icon_data.except(:elements).merge(html_options)) do
28
+ template.safe_join(
29
+ icon_data[:elements].map do |element|
30
+ template.content_tag(element[:tag], nil, element.except(:tag))
31
+ end
38
32
  )
39
33
  end
40
34
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class InputGroup < Plumber::Base
6
+ def render(leading: nil, trailing: nil, error: false, **kwargs, &block)
7
+ html_options = merge_html_options(
8
+ theme.resolve(:input_group, error: error),
9
+ kwargs
10
+ )
11
+ template.content_tag(:div, **html_options) do
12
+ build_input_group(leading, trailing, &block)
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def build_input_group(leading, trailing, &block)
19
+ template.safe_join(
20
+ [
21
+ leading.respond_to?(:call) ? template.capture(&leading) : leading,
22
+ template.capture(&block),
23
+ trailing.respond_to?(:call) ? template.capture(&trailing) : trailing
24
+ ]
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
@@ -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
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class List
6
+ class Item < Plumber::Base
7
+ def render(content = nil, **kwargs, &block)
8
+ slots = List::Item::Slots.new
9
+ slots.with_title(content) if content
10
+ slots.with_icon_trailing("external-link") if kwargs[:url].present? && kwargs[:target] == "_blank"
11
+ yield slots if block_given?
12
+
13
+ template.content_tag(:li) do
14
+ build(**kwargs) do |attrs|
15
+ render_link_or_button(**attrs) { render_item_slots(slots) }
16
+ end
17
+ end
18
+ end
19
+
20
+ def build(**kwargs, &block)
21
+ html_options = merge_html_options(theme.resolve(:list_item), kwargs)
22
+ template.capture(html_options, &block)
23
+ end
24
+
25
+ private
26
+
27
+ def render_link_or_button(url: nil, target: nil, active: false, **html_options, &block)
28
+ if url.present?
29
+ aria = active ? { aria: { current: "page" } } : {}
30
+ template.content_tag(:a, href: url, target: target, **merge_html_options(html_options, aria)) do
31
+ template.capture(&block)
32
+ end
33
+ else
34
+ aria = active ? { aria: { current: true } } : {}
35
+ template.content_tag(:button, type: "button", **merge_html_options(html_options, aria)) do
36
+ template.capture(&block)
37
+ end
38
+ end
39
+ end
40
+
41
+ def render_icon_slot(slots, name)
42
+ slots.resolve(name) do |value|
43
+ next value unless Components::Icon.icon_name?(value)
44
+
45
+ Components::Icon.new(template).render(
46
+ name: value,
47
+ classes: theme.resolve(:list_item_icon).fetch(:classes, ""),
48
+ aria: { hidden: "true" }
49
+ )
50
+ end
51
+ end
52
+
53
+ def render_title_slot(slots)
54
+ slots.resolve(:title) { |v| template.content_tag(:span, v, **merge_html_options(theme.resolve(:list_item_title))) }
55
+ end
56
+
57
+ def render_description_slot(slots)
58
+ slots.resolve(:description) do |v|
59
+ template.content_tag(:span, v, **merge_html_options(theme.resolve(:list_item_description)))
60
+ end
61
+ end
62
+
63
+ def render_content_slot(slots)
64
+ title = render_title_slot(slots)
65
+ description = render_description_slot(slots)
66
+ return unless title || description
67
+
68
+ template.content_tag(:span, **merge_html_options(theme.resolve(:list_item_content))) do
69
+ template.safe_join([title, description].compact)
70
+ end
71
+ end
72
+
73
+ def render_item_slots(slots)
74
+ icon_leading = render_icon_slot(slots, :icon_leading)
75
+ icon_trailing = render_icon_slot(slots, :icon_trailing)
76
+ content = render_content_slot(slots)
77
+
78
+ template.safe_join([icon_leading, content, icon_trailing].compact)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class List
6
+ class Section < Plumber::Base
7
+ def initialize(template, heading_level: nil)
8
+ super(template)
9
+ @heading_level = heading_level
10
+ end
11
+
12
+ def render(title: nil, description: nil, **kwargs, &block)
13
+ html_options = merge_html_options(theme.resolve(:list_section), kwargs)
14
+ template.content_tag(:li, **html_options) do
15
+ template.safe_join(
16
+ [
17
+ render_section_header(title, description),
18
+ render_section_body(title, &block)
19
+ ]
20
+ )
21
+ end
22
+ end
23
+
24
+ def section(...)
25
+ List::Section.new(template, heading_level: (@heading_level || 0) + 1).render(...)
26
+ end
27
+
28
+ def item(content = nil, **kwargs, &block)
29
+ List::Item.new(template).render(content, **kwargs, &block)
30
+ end
31
+
32
+ private
33
+
34
+ def render_section_header(title, description)
35
+ return unless title.present? || description.present?
36
+
37
+ template.safe_join(
38
+ [
39
+ render_section_title(title),
40
+ (if description.present?
41
+ template.content_tag(
42
+ :span,
43
+ description,
44
+ **merge_html_options(theme.resolve(:list_section_description))
45
+ )
46
+ end)
47
+ ].compact
48
+ )
49
+ end
50
+
51
+ def render_section_title(title)
52
+ return unless title.present?
53
+
54
+ if @heading_level
55
+ tag = :"h#{[@heading_level, 6].min}"
56
+ template.content_tag(tag, title, **merge_html_options(theme.resolve(:list_section_title)))
57
+ else
58
+ template.content_tag(
59
+ :span,
60
+ title,
61
+ **merge_html_options(theme.resolve(:list_section_title), { aria: { hidden: "true" } })
62
+ )
63
+ end
64
+ end
65
+
66
+ def render_section_body(title, &block)
67
+ opts = title.present? ? { aria: { label: title } } : {}
68
+ template.content_tag(:ul, template.capture(self, &block), opts)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class List < Plumber::Base
6
+ def render(...)
7
+ render_list(...)
8
+ end
9
+
10
+ def section(...)
11
+ List::Section.new(template, heading_level: @heading_level).render(...)
12
+ end
13
+
14
+ def item(content = nil, **kwargs, &block)
15
+ List::Item.new(template).render(content, **kwargs, &block)
16
+ end
17
+
18
+ private
19
+
20
+ def render_list(role: "list", heading_level: nil, **kwargs, &block)
21
+ @heading_level = heading_level
22
+ html_options = merge_html_options(
23
+ theme.resolve(:list),
24
+ kwargs,
25
+ { role: role }
26
+ )
27
+ template.content_tag(:ul, template.capture(self, &block), **html_options)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Popover
6
+ class Panel < Plumber::Base
7
+ def render(panel_id:, tag: :div, **kwargs, &block)
8
+ template.content_tag(
9
+ tag,
10
+ block_given? ? template.capture(panel_id, &block) : nil,
11
+ **panel_attrs(panel_id, **kwargs)
12
+ )
13
+ end
14
+
15
+ def build(panel_id:, **kwargs, &block)
16
+ template.capture(panel_attrs(panel_id, **kwargs), &block)
17
+ end
18
+
19
+ private
20
+
21
+ def panel_attrs(panel_id, **kwargs)
22
+ merge_html_options(
23
+ { id: panel_id, hidden: "" },
24
+ theme.resolve(:popover),
25
+ { data: { popover_target: "panel" } },
26
+ kwargs
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Popover
6
+ class Trigger < Plumber::Base
7
+ STIMULUS_ACTION = [
8
+ "click->#{STIMULUS_CONTROLLER}#toggle",
9
+ "keydown.esc->#{STIMULUS_CONTROLLER}#close"
10
+ ].join(" ").freeze
11
+
12
+ def render(panel_id:, haspopup: "dialog", **kwargs, &block)
13
+ html_options = merge_html_options(
14
+ theme.resolve(:popover_trigger),
15
+ {
16
+ type: "button",
17
+ aria: { haspopup: haspopup, expanded: "false", controls: panel_id },
18
+ data: { popover_target: "trigger", action: STIMULUS_ACTION }
19
+ },
20
+ kwargs
21
+ )
22
+ template.content_tag(:button, block_given? ? template.capture(&block) : nil, **html_options)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end