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
@@ -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])
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])
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
+ ]
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
@@ -3,31 +3,57 @@
3
3
  module StimulusPlumbers
4
4
  module Components
5
5
  class Popover < Plumber::Base
6
- def render(...)
7
- render_popover(...)
8
- end
6
+ STIMULUS_CONTROLLER = "popover"
9
7
 
10
- private
8
+ def self.panel_id_for(trigger_id)
9
+ [trigger_id, "popover"].compact.join("_")
10
+ end
11
11
 
12
- def render_popover(interactive: true, **kwargs, &block)
13
- html_options = merge_html_options(
14
- { classes: theme.resolve(:popover).fetch(:classes, "") },
15
- kwargs
16
- )
12
+ def render(...) = render_popover(...)
17
13
 
18
- builder = Popover::Builder.new(template)
19
- template.capture(builder, &block)
14
+ def build(panel_id: nil, &block)
15
+ @panel_id = panel_id || self.class.panel_id_for(template.sp_dom_id)
16
+ @trigger_html = nil
17
+ @panel_html = nil
18
+ yield self
19
+ template.safe_join([@trigger_html, @panel_html])
20
+ end
20
21
 
21
- template.content_tag(:div, **html_options) do
22
- template.safe_join([builder.activator_html, wrap_content(interactive, builder)])
22
+ def trigger(haspopup: "dialog", controls: @panel_id, **kwargs, &block)
23
+ if block_given? && block.arity == 1
24
+ attrs = {
25
+ panel_id: @panel_id,
26
+ aria: { haspopup: haspopup, expanded: "false", controls: controls },
27
+ data: { popover_target: "trigger", action: Popover::Trigger::STIMULUS_ACTION }
28
+ }
29
+ @trigger_html = template.capture(attrs, &block)
30
+ else
31
+ @trigger_html = Popover::Trigger.new(template).render(
32
+ panel_id: @panel_id, haspopup: haspopup, **kwargs, &block
33
+ )
23
34
  end
24
35
  end
25
36
 
26
- def wrap_content(interactive, builder)
27
- if interactive
28
- template.content_tag(:template, builder.content_html)
29
- else
30
- builder.content_html
37
+ def panel(**kwargs, &block)
38
+ @panel_html = Popover::Panel.new(template).render(panel_id: @panel_id, **kwargs, &block)
39
+ end
40
+
41
+ def build_panel(**kwargs, &block)
42
+ @panel_html = Popover::Panel.new(template).build(panel_id: @panel_id, **kwargs, &block)
43
+ end
44
+
45
+ private
46
+
47
+ def render_popover(panel_id: nil, close_on_select: nil, **kwargs, &block)
48
+ data = { controller: STIMULUS_CONTROLLER }
49
+ data[:popover_close_on_select_value] = close_on_select unless close_on_select.nil?
50
+ html_options = merge_html_options(
51
+ theme.resolve(:popover_wrapper),
52
+ kwargs,
53
+ { data: data }
54
+ )
55
+ template.content_tag(:div, **html_options) do
56
+ build(panel_id: panel_id, &block)
31
57
  end
32
58
  end
33
59
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Timeline
6
+ class Event
7
+ class Slots < Plumber::Slots
8
+ slot :indicator, :time, :title, :trigger, :description, :detail, :actions
9
+
10
+ def with_indicator(icon: nil)
11
+ set_slot(:indicator, icon, { type: icon ? :icon : :dot, icon: icon })
12
+ nil
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Timeline
6
+ class Event < Plumber::Base
7
+ attr_reader :interactive
8
+
9
+ def self.detail_id_for(event_id)
10
+ [event_id, "detail"].compact.join("_")
11
+ end
12
+
13
+ def render(datetime: nil, id: nil, interactive: false, orientation: :vertical, **kwargs, &block)
14
+ @detail_id = self.class.detail_id_for(id || template.sp_dom_id)
15
+
16
+ slots = Timeline::Event::Slots.new
17
+ yield slots if block_given?
18
+
19
+ validate_slots!(slots, datetime: datetime)
20
+ @orientation = orientation
21
+ @interactive = interactive || slots.trigger?
22
+
23
+ html_options = merge_html_options(theme.resolve(:timeline_item, orientation: orientation), kwargs)
24
+ render_event(slots, datetime: datetime, html_options: html_options)
25
+ end
26
+
27
+ private
28
+
29
+ def validate_slots!(slots, datetime:)
30
+ raise ArgumentError, "e.title and e.trigger are mutually exclusive" if slots.title? && slots.trigger?
31
+ raise ArgumentError, "e.detail requires e.trigger" if slots.detail? && !slots.trigger?
32
+ raise ArgumentError, "e.time requires datetime:" if slots.time? && datetime.nil?
33
+ end
34
+
35
+ def render_event(slots, datetime:, html_options:)
36
+ template.content_tag(:li, **html_options) do
37
+ if @orientation.to_sym == :horizontal
38
+ render_horizontal_event(slots, datetime: datetime)
39
+ else
40
+ render_vertical_event(slots, datetime: datetime)
41
+ end
42
+ end
43
+ end
44
+
45
+ def render_vertical_event(slots, datetime:)
46
+ content_col = template.content_tag(:div, class: "flex-1 min-w-0") do
47
+ template.safe_join(
48
+ [
49
+ render_time(slots, datetime: datetime),
50
+ render_heading(slots),
51
+ render_description(slots),
52
+ render_detail(slots),
53
+ render_actions(slots)
54
+ ].compact
55
+ )
56
+ end
57
+ template.safe_join([render_indicator(slots), content_col].compact)
58
+ end
59
+
60
+ def render_horizontal_event(slots, datetime:)
61
+ template.safe_join(
62
+ [
63
+ render_horizontal_indicator_row(slots),
64
+ render_horizontal_content(slots, datetime: datetime)
65
+ ]
66
+ )
67
+ end
68
+
69
+ def render_horizontal_indicator_row(slots)
70
+ template.content_tag(:div, class: "flex items-center") do
71
+ template.safe_join(
72
+ [
73
+ render_indicator(slots),
74
+ template.content_tag(:div, nil, **merge_html_options(theme.resolve(:timeline_item_connector)))
75
+ ].compact
76
+ )
77
+ end
78
+ end
79
+
80
+ def render_horizontal_content(slots, datetime:)
81
+ template.content_tag(:div, **merge_html_options(theme.resolve(:timeline_item_content))) do
82
+ template.safe_join(
83
+ [
84
+ render_time(slots, datetime: datetime),
85
+ render_heading(slots),
86
+ render_description(slots),
87
+ render_detail(slots),
88
+ render_actions(slots)
89
+ ].compact
90
+ )
91
+ end
92
+ end
93
+
94
+ def render_indicator(slots)
95
+ return unless slots.indicator?
96
+
97
+ type = slots.options_for(:indicator)[:type]
98
+ icon_name = slots.options_for(:indicator)[:icon]
99
+ html_options = merge_html_options(
100
+ theme.resolve(:timeline_item_indicator, type: type, orientation: @orientation),
101
+ { aria: { hidden: "true" } }
102
+ )
103
+ template.content_tag(:div, **html_options) { render_indicator_content(type: type, icon_name: icon_name) }
104
+ end
105
+
106
+ def render_indicator_content(type:, icon_name:)
107
+ if type == :icon && icon_name
108
+ Components::Icon.new(template).render(
109
+ name: icon_name,
110
+ classes: theme.resolve(:timeline_item_indicator_icon_slot).fetch(:classes, ""),
111
+ aria: { hidden: "true" }
112
+ )
113
+ else
114
+ template.content_tag(:span, nil, **merge_html_options(theme.resolve(:timeline_item_indicator_dot)))
115
+ end
116
+ end
117
+
118
+ def render_time(slots, datetime:)
119
+ return unless datetime
120
+
121
+ content = slots.resolve(:time)
122
+ time_type = slots.options_for(:time)[:type] || :default
123
+ time_target = @interactive ? { data: { "timeline-target": "time" } } : {}
124
+ attrs = merge_html_options(theme.resolve(:timeline_item_time, type: time_type), { datetime: datetime }, time_target)
125
+ template.content_tag(:time, content, **attrs)
126
+ end
127
+
128
+ def render_heading(slots)
129
+ if slots.trigger?
130
+ render_trigger_heading(slots)
131
+ elsif slots.title?
132
+ content = slots.resolve(:title)
133
+ template.content_tag(:h3, content, **merge_html_options(theme.resolve(:timeline_item_title)))
134
+ end
135
+ end
136
+
137
+ def render_trigger_heading(slots)
138
+ content = slots.resolve(:trigger)
139
+ trigger_attrs = if @interactive
140
+ merge_html_options(
141
+ theme.resolve(:timeline_item_trigger),
142
+ {
143
+ data: { "timeline-target": "trigger", action: "timeline#toggle" },
144
+ aria: { expanded: "false", controls: @detail_id }
145
+ }
146
+ )
147
+ else
148
+ merge_html_options(theme.resolve(:timeline_item_trigger))
149
+ end
150
+ button = template.content_tag(:button, content, type: "button", **trigger_attrs)
151
+ template.content_tag(:h3, button, **merge_html_options(theme.resolve(:timeline_item_heading)))
152
+ end
153
+
154
+ def render_description(slots)
155
+ return unless slots.description?
156
+
157
+ content = slots.resolve(:description)
158
+ return unless content.present?
159
+
160
+ template.content_tag(:p, content, **merge_html_options(theme.resolve(:timeline_item_description)))
161
+ end
162
+
163
+ def render_detail(slots)
164
+ return unless slots.detail?
165
+
166
+ content = slots.resolve(:detail)
167
+ detail_attrs = if @interactive
168
+ merge_html_options(
169
+ theme.resolve(:timeline_item_detail),
170
+ { id: @detail_id, hidden: "", data: { "timeline-target": "detail" } }
171
+ )
172
+ else
173
+ merge_html_options(theme.resolve(:timeline_item_detail))
174
+ end
175
+ template.content_tag(:div, content, **detail_attrs)
176
+ end
177
+
178
+ def render_actions(slots)
179
+ return unless slots.actions?
180
+
181
+ content = slots.resolve(:actions)
182
+ return unless content.present?
183
+
184
+ template.content_tag(:div, content, **merge_html_options(theme.resolve(:timeline_item_actions)))
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Timeline
6
+ class Group < Plumber::Base
7
+ def render(**kwargs, &block)
8
+ content = template.capture(self, &block)
9
+ html_options = merge_html_options(theme.resolve(:timeline_group), kwargs)
10
+ template.content_tag(:div, content, **html_options)
11
+ end
12
+
13
+ def section(date:, datetime: nil, **kwargs, &block)
14
+ inner_tl = Timeline.new(template)
15
+ inner_tl.instance_variable_set(:@orientation, :vertical)
16
+ inner_tl.instance_variable_set(:@interactive, false)
17
+
18
+ events_html = template.capture(inner_tl, &block)
19
+ section_html = template.content_tag(:div, **merge_html_options(theme.resolve(:timeline_group_section), kwargs)) do
20
+ render_section_body(date: date, datetime: datetime, events_html: events_html)
21
+ end
22
+ template.concat(section_html)
23
+ end
24
+
25
+ private
26
+
27
+ def render_section_body(date:, datetime:, events_html:)
28
+ date_attrs = merge_html_options(
29
+ theme.resolve(:timeline_group_section_date),
30
+ datetime ? { datetime: datetime } : {}
31
+ )
32
+ template.safe_join(
33
+ [
34
+ template.content_tag(:time, date, **date_attrs),
35
+ template.content_tag(:ol, events_html, **merge_html_options(theme.resolve(:timeline_group_section_list)))
36
+ ]
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Timeline < Plumber::Base
6
+ def render(orientation: :vertical, interactive: false, **kwargs, &block)
7
+ @interactive = interactive
8
+ @orientation = orientation
9
+ content = template.capture(self, &block)
10
+ stimulus = @interactive ? { data: { controller: "timeline" } } : {}
11
+ ol_options = merge_html_options(theme.resolve(:timeline, orientation: orientation), kwargs, stimulus)
12
+ list = template.content_tag(:ol, content, **ol_options)
13
+
14
+ orientation.to_sym == :vertical ? render_vertical_wrapper(list) : list
15
+ end
16
+
17
+ def event(datetime: nil, **kwargs, &block)
18
+ ev = Timeline::Event.new(template)
19
+ html = ev.render(
20
+ datetime: datetime,
21
+ interactive: @interactive,
22
+ orientation: @orientation,
23
+ **kwargs,
24
+ &block
25
+ )
26
+ @interactive ||= ev.interactive
27
+ template.concat(html)
28
+ end
29
+
30
+ private
31
+
32
+ def render_vertical_wrapper(list)
33
+ track_line = template.content_tag(:div, nil, **merge_html_options(theme.resolve(:timeline_track_line)))
34
+ template.content_tag(:div, class: "relative") do
35
+ template.safe_join([track_line, list])
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -7,6 +7,7 @@ module StimulusPlumbers
7
7
  isolate_namespace StimulusPlumbers
8
8
 
9
9
  config.autoload_paths << File.expand_path("../stimulus-plumbers", __dir__)
10
+ config.i18n.load_path += Dir[File.expand_path("../../config/locales/*.{rb,yml}", __dir__)]
10
11
 
11
12
  initializer "stimulus_plumbers.assets", after: :set_default_precompile do |app|
12
13
  app.config.assets.precompile += %w[stimulus_plumbers/tokens.css] if app.config.respond_to?(:assets)