stimulus_plumbers 0.3.3 → 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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/app/assets/javascripts/stimulus-plumbers/controllers.manifest.json +273 -0
  4. data/app/assets/javascripts/stimulus-plumbers/stimulus-plumbers-controllers.es.js +228 -145
  5. data/app/assets/javascripts/stimulus-plumbers/stimulus-plumbers-controllers.umd.js +1 -1
  6. data/app/assets/stylesheets/stimulus_plumbers/tokens.css +43 -7
  7. data/config/locales/en.yml +10 -0
  8. data/lib/stimulus_plumbers/components/avatar.rb +14 -13
  9. data/lib/stimulus_plumbers/components/button/group.rb +9 -4
  10. data/lib/stimulus_plumbers/components/button/slots.rb +11 -0
  11. data/lib/stimulus_plumbers/components/button.rb +30 -34
  12. data/lib/stimulus_plumbers/components/calendar/turbo/days_of_month.rb +151 -0
  13. data/lib/stimulus_plumbers/components/calendar/turbo/days_of_week.rb +62 -0
  14. data/lib/stimulus_plumbers/components/calendar/turbo/months_of_year.rb +99 -0
  15. data/lib/stimulus_plumbers/components/calendar/turbo/years_of_decade.rb +86 -0
  16. data/lib/stimulus_plumbers/components/calendar/turbo.rb +65 -0
  17. data/lib/stimulus_plumbers/components/calendar.rb +70 -29
  18. data/lib/stimulus_plumbers/components/card/slots.rb +26 -0
  19. data/lib/stimulus_plumbers/components/card.rb +54 -14
  20. data/lib/stimulus_plumbers/components/combobox/builder.rb +45 -0
  21. data/lib/stimulus_plumbers/components/combobox/date/navigation.rb +72 -0
  22. data/lib/stimulus_plumbers/components/combobox/date/navigator.rb +25 -0
  23. data/lib/stimulus_plumbers/components/combobox/date.rb +34 -24
  24. data/lib/stimulus_plumbers/components/combobox/dropdown.rb +27 -24
  25. data/lib/stimulus_plumbers/components/combobox/options/option.rb +1 -1
  26. data/lib/stimulus_plumbers/components/combobox/options/option_group.rb +1 -1
  27. data/lib/stimulus_plumbers/components/combobox/time/drum.rb +1 -1
  28. data/lib/stimulus_plumbers/components/combobox/time.rb +48 -49
  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/link/slots.rb +11 -0
  35. data/lib/stimulus_plumbers/components/link.rb +63 -0
  36. data/lib/stimulus_plumbers/components/list/item/slots.rb +13 -0
  37. data/lib/stimulus_plumbers/components/list/item.rb +83 -0
  38. data/lib/stimulus_plumbers/components/list/section.rb +73 -0
  39. data/lib/stimulus_plumbers/components/list.rb +31 -0
  40. data/lib/stimulus_plumbers/components/popover/panel.rb +32 -0
  41. data/lib/stimulus_plumbers/components/popover/trigger.rb +27 -0
  42. data/lib/stimulus_plumbers/components/popover.rb +44 -18
  43. data/lib/stimulus_plumbers/engine.rb +1 -0
  44. data/lib/stimulus_plumbers/form/base.rb +103 -0
  45. data/lib/stimulus_plumbers/form/builder.rb +71 -24
  46. data/lib/stimulus_plumbers/form/field.rb +56 -88
  47. data/lib/stimulus_plumbers/form/fields/error.rb +1 -1
  48. data/lib/stimulus_plumbers/form/fields/fieldset.rb +11 -8
  49. data/lib/stimulus_plumbers/form/fields/hint.rb +1 -1
  50. data/lib/stimulus_plumbers/form/fields/inputs/checkbox.rb +115 -0
  51. data/lib/stimulus_plumbers/form/fields/inputs/combobox.rb +24 -0
  52. data/lib/stimulus_plumbers/form/fields/inputs/datetime.rb +40 -58
  53. data/lib/stimulus_plumbers/form/fields/inputs/file.rb +9 -8
  54. data/lib/stimulus_plumbers/form/fields/inputs/password.rb +30 -23
  55. data/lib/stimulus_plumbers/form/fields/inputs/radio.rb +60 -0
  56. data/lib/stimulus_plumbers/form/fields/inputs/search.rb +31 -54
  57. data/lib/stimulus_plumbers/form/fields/inputs/select/grouped.rb +22 -33
  58. data/lib/stimulus_plumbers/form/fields/inputs/select/timezone.rb +3 -46
  59. data/lib/stimulus_plumbers/form/fields/inputs/select/weekday.rb +3 -26
  60. data/lib/stimulus_plumbers/form/fields/inputs/select.rb +62 -61
  61. data/lib/stimulus_plumbers/form/fields/inputs/submit.rb +10 -7
  62. data/lib/stimulus_plumbers/form/fields/inputs/text.rb +29 -22
  63. data/lib/stimulus_plumbers/form/fields/inputs/text_area.rb +9 -8
  64. data/lib/stimulus_plumbers/form/fields/label/floating.rb +41 -0
  65. data/lib/stimulus_plumbers/form/fields/label.rb +9 -3
  66. data/lib/stimulus_plumbers/form/fields/renderer.rb +39 -0
  67. data/lib/stimulus_plumbers/helpers/button_helper.rb +1 -1
  68. data/lib/stimulus_plumbers/helpers/calendar_helper.rb +2 -2
  69. data/lib/stimulus_plumbers/helpers/calendar_turbo_helper.rb +56 -4
  70. data/lib/stimulus_plumbers/helpers/card_helper.rb +1 -11
  71. data/lib/stimulus_plumbers/helpers/combobox_helper.rb +27 -60
  72. data/lib/stimulus_plumbers/helpers/icon_helper.rb +11 -0
  73. data/lib/stimulus_plumbers/helpers/link_helper.rb +11 -0
  74. data/lib/stimulus_plumbers/helpers/list_helper.rb +11 -0
  75. data/lib/stimulus_plumbers/helpers/plumber_helper.rb +3 -6
  76. data/lib/stimulus_plumbers/helpers.rb +6 -2
  77. data/lib/stimulus_plumbers/logger.rb +4 -3
  78. data/lib/stimulus_plumbers/plumber/base.rb +6 -1
  79. data/lib/stimulus_plumbers/plumber/dispatcher/klass_proxy.rb +4 -3
  80. data/lib/stimulus_plumbers/plumber/dispatcher/method_call.rb +4 -3
  81. data/lib/stimulus_plumbers/plumber/dispatcher.rb +4 -4
  82. data/lib/stimulus_plumbers/plumber/options/aria.rb +17 -0
  83. data/lib/stimulus_plumbers/plumber/options/html.rb +29 -0
  84. data/lib/stimulus_plumbers/plumber/options/stimulus.rb +29 -0
  85. data/lib/stimulus_plumbers/plumber/options/theme.rb +19 -0
  86. data/lib/stimulus_plumbers/plumber/options/token_list.rb +29 -0
  87. data/lib/stimulus_plumbers/plumber/renderer.rb +136 -41
  88. data/lib/stimulus_plumbers/plumber/slots.rb +74 -0
  89. data/lib/stimulus_plumbers/themes/base.rb +5 -7
  90. data/lib/stimulus_plumbers/themes/schema/avatar/ranges.rb +13 -0
  91. data/lib/stimulus_plumbers/themes/schema/button/ranges.rb +16 -0
  92. data/lib/stimulus_plumbers/themes/schema/card/ranges.rb +13 -0
  93. data/lib/stimulus_plumbers/themes/schema/form/checkbox/ranges.rb +16 -0
  94. data/lib/stimulus_plumbers/themes/schema/form/radio/ranges.rb +16 -0
  95. data/lib/stimulus_plumbers/themes/schema/form/ranges.rb +1 -2
  96. data/lib/stimulus_plumbers/themes/schema/link/ranges.rb +14 -0
  97. data/lib/stimulus_plumbers/themes/schema/ranges.rb +1 -5
  98. data/lib/stimulus_plumbers/themes/schema.rb +119 -48
  99. data/lib/stimulus_plumbers/version.rb +1 -1
  100. data/lib/stimulus_plumbers.rb +20 -15
  101. metadata +42 -15
  102. data/lib/stimulus_plumbers/components/action_list/item.rb +0 -30
  103. data/lib/stimulus_plumbers/components/action_list/section.rb +0 -28
  104. data/lib/stimulus_plumbers/components/action_list.rb +0 -29
  105. data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_month.rb +0 -149
  106. data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_week.rb +0 -43
  107. data/lib/stimulus_plumbers/components/calendar/month/turbo.rb +0 -59
  108. data/lib/stimulus_plumbers/components/card/section.rb +0 -31
  109. data/lib/stimulus_plumbers/components/combobox/popover.rb +0 -47
  110. data/lib/stimulus_plumbers/components/date_picker/navigation.rb +0 -41
  111. data/lib/stimulus_plumbers/components/date_picker/navigator.rb +0 -23
  112. data/lib/stimulus_plumbers/components/popover/builder.rb +0 -25
  113. data/lib/stimulus_plumbers/form/fields/inputs/choice.rb +0 -69
  114. data/lib/stimulus_plumbers/helpers/action_list_helper.rb +0 -25
  115. data/lib/stimulus_plumbers/plumber/html_options.rb +0 -52
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Calendar
6
+ class Turbo
7
+ class DaysOfWeek < Plumber::Base
8
+ WEEKDAY_FORMATS = %i[narrow short long].freeze
9
+
10
+ attr_reader :format
11
+
12
+ def initialize(template, format: :short)
13
+ super(template)
14
+ @format = format
15
+ end
16
+
17
+ def render(...)
18
+ render_days_of_week(...)
19
+ end
20
+
21
+ private
22
+
23
+ def render_days_of_week(**kwargs)
24
+ html_options = merge_html_options(
25
+ theme.resolve(:calendar_days_of_week),
26
+ kwargs
27
+ )
28
+ template.content_tag(:div, **html_options) { days_of_week }
29
+ end
30
+
31
+ def days_of_week
32
+ week_options = merge_html_options(
33
+ theme.resolve(:calendar_row),
34
+ { role: "row" }
35
+ )
36
+ template.content_tag(:div, **week_options) do
37
+ template.safe_join(
38
+ day_names.map do |abbr, full|
39
+ options = { role: "columnheader" }
40
+ options[:aria] = { label: abbr } if format == :narrow
41
+ template.content_tag(:span, display_name(abbr, full), **options)
42
+ end
43
+ )
44
+ end
45
+ end
46
+
47
+ def display_name(abbr, full)
48
+ case format
49
+ when :narrow then abbr[0, 1]
50
+ when :long then full
51
+ else abbr
52
+ end
53
+ end
54
+
55
+ def day_names
56
+ I18n.t("date.abbr_day_names").zip(I18n.t("date.day_names"))
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Calendar
6
+ class Turbo
7
+ class MonthsOfYear < Plumber::Base
8
+ MONTHS_PER_ROW = 4
9
+ MONTH_FORMATS = %i[narrow short long].freeze
10
+
11
+ attr_reader :date, :today, :selected_date, :format
12
+
13
+ def initialize(template, date: Date.today, today: Date.today, selected_date: nil, format: :short)
14
+ super(template)
15
+ @date = date
16
+ @today = today
17
+ @selected_date = selected_date
18
+ @format = format
19
+ end
20
+
21
+ def render(...)
22
+ render_months_of_year(...)
23
+ end
24
+
25
+ private
26
+
27
+ def render_months_of_year(**html_options)
28
+ html_options = merge_html_options(
29
+ theme.resolve(:calendar_months_of_year),
30
+ html_options
31
+ )
32
+ template.content_tag(:div, role: "rowgroup", **html_options) { months_of_year }
33
+ end
34
+
35
+ def months_of_year
36
+ row_options = merge_html_options(
37
+ theme.resolve(:calendar_row),
38
+ { role: "row" }
39
+ )
40
+ template.safe_join(
41
+ month_names.each_slice(MONTHS_PER_ROW).map do |months|
42
+ template.content_tag(:div, **row_options) { months_in_row(months) }
43
+ end
44
+ )
45
+ end
46
+
47
+ def month_names
48
+ I18n.t("date.abbr_month_names").compact
49
+ .zip(I18n.t("date.month_names").compact)
50
+ .each_with_index.map { |(abbr, full), i| [i + 1, abbr, full] }
51
+ end
52
+
53
+ def months_in_row(months)
54
+ template.safe_join(months.map { |number, abbr, full| month_cell(number, abbr, full) })
55
+ end
56
+
57
+ def month_cell(month_number, abbr, full)
58
+ options = month_cell_html_options(month_number)
59
+ options[:aria][:label] = abbr if format == :narrow
60
+ template.content_tag(:button, display_name(abbr, full), **options)
61
+ end
62
+
63
+ def display_name(abbr, full)
64
+ case format
65
+ when :narrow then abbr[0, 1]
66
+ when :long then full
67
+ else abbr
68
+ end
69
+ end
70
+
71
+ def month_cell_html_options(month_number)
72
+ is_current_month = month_number == today.month && date.year == today.year
73
+ is_focused = selected_date_in_month?(month_number) || (is_current_month && !selected_date_in_current_year?)
74
+ merge_html_options(
75
+ theme.resolve(:calendar_month),
76
+ {
77
+ role: "gridcell",
78
+ tabindex: is_focused ? 0 : -1,
79
+ data: { month: month_number },
80
+ aria: {
81
+ current: is_current_month ? "month" : nil,
82
+ selected: selected_date_in_month?(month_number) ? "true" : "false"
83
+ }
84
+ }
85
+ )
86
+ end
87
+
88
+ def selected_date_in_current_year?
89
+ selected_date && selected_date.year == date.year
90
+ end
91
+
92
+ def selected_date_in_month?(month)
93
+ selected_date && month == selected_date.month && date.year == selected_date.year
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Calendar
6
+ class Turbo
7
+ class YearsOfDecade < Plumber::Base
8
+ YEARS_PER_ROW = 4
9
+ DECADE_SIZE = 10
10
+
11
+ attr_reader :date, :today, :selected_date
12
+
13
+ def initialize(template, date: Date.today, today: Date.today, selected_date: nil)
14
+ super(template)
15
+ @date = date
16
+ @today = today
17
+ @selected_date = selected_date
18
+ end
19
+
20
+ def render(...)
21
+ render_years_of_decade(...)
22
+ end
23
+
24
+ private
25
+
26
+ def render_years_of_decade(**html_options)
27
+ html_options = merge_html_options(
28
+ theme.resolve(:calendar_years_of_decade),
29
+ html_options
30
+ )
31
+ template.content_tag(:div, role: "rowgroup", **html_options) { years_of_decade }
32
+ end
33
+
34
+ def years_of_decade
35
+ row_options = merge_html_options(
36
+ theme.resolve(:calendar_row),
37
+ { role: "row" }
38
+ )
39
+ template.safe_join(
40
+ year_names.each_slice(YEARS_PER_ROW).map do |years|
41
+ template.content_tag(:div, **row_options) { years_in_row(years) }
42
+ end
43
+ )
44
+ end
45
+
46
+ def year_names
47
+ decade_start = (date.year / DECADE_SIZE) * DECADE_SIZE
48
+ ((decade_start - 1)..(decade_start + DECADE_SIZE)).map do |y|
49
+ [y, y < decade_start || y > decade_start + DECADE_SIZE - 1]
50
+ end
51
+ end
52
+
53
+ def years_in_row(years)
54
+ template.safe_join(years.map { |year, outside| year_cell(year, outside) })
55
+ end
56
+
57
+ def year_cell(year, outside)
58
+ template.content_tag(:button, year.to_s, **year_cell_html_options(year, outside))
59
+ end
60
+
61
+ def year_cell_html_options(year, outside)
62
+ is_current_year = year == today.year
63
+ is_focused = selected_date_in_year?(year) || (is_current_year && !selected_date)
64
+ merge_html_options(
65
+ theme.resolve(:calendar_year, outside: outside),
66
+ {
67
+ role: "gridcell",
68
+ tabindex: is_focused ? 0 : -1,
69
+ data: { year: year },
70
+ aria: {
71
+ current: is_current_year ? "year" : nil,
72
+ selected: selected_date_in_year?(year) ? "true" : "false",
73
+ disabled: outside ? "true" : nil
74
+ }
75
+ }
76
+ )
77
+ end
78
+
79
+ def selected_date_in_year?(year)
80
+ selected_date && year == selected_date.year
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Calendar
6
+ class Turbo < Plumber::Base
7
+ STIMULUS_CONTROLLER = "calendar-observer"
8
+ STIMULUS_ACTION = "click->#{STIMULUS_CONTROLLER}#onSelect".freeze
9
+
10
+ def month(
11
+ date: Date.today,
12
+ today: Date.today,
13
+ show_other_months: false,
14
+ weekday_format: :short,
15
+ **kwargs
16
+ )
17
+ selectable = kwargs.delete(:selectable) { false }
18
+ selected_date = kwargs.delete(:selected_date) { nil }
19
+ html_options = merge_html_options(
20
+ theme.resolve(:calendar),
21
+ kwargs,
22
+ { data: { controller: STIMULUS_CONTROLLER, action: STIMULUS_ACTION } }
23
+ )
24
+ template.content_tag(:div, role: "grid", **html_options) do
25
+ template.safe_join(
26
+ [
27
+ Turbo::DaysOfWeek.new(template, format: weekday_format).render,
28
+ Turbo::DaysOfMonth.new(
29
+ template,
30
+ date: date,
31
+ today: today,
32
+ selectable: selectable,
33
+ selected_date: selected_date,
34
+ show_other_months: show_other_months
35
+ ).render
36
+ ]
37
+ )
38
+ end
39
+ end
40
+
41
+ def year(date: Date.today, today: Date.today, selected_date: nil, month_format: :short, **kwargs)
42
+ html_options = merge_html_options(
43
+ theme.resolve(:calendar_quarter_grid),
44
+ kwargs,
45
+ { role: "grid", aria: { label: "Year view" } }
46
+ )
47
+ template.content_tag(:div, **html_options) do
48
+ Turbo::MonthsOfYear.new(template, date: date, today: today, selected_date: selected_date, format: month_format).render
49
+ end
50
+ end
51
+
52
+ def decade(date: Date.today, today: Date.today, selected_date: nil, **kwargs)
53
+ html_options = merge_html_options(
54
+ theme.resolve(:calendar_quarter_grid),
55
+ kwargs,
56
+ { role: "grid", aria: { label: "Decade view" } }
57
+ )
58
+ template.content_tag(:div, **html_options) do
59
+ Turbo::YearsOfDecade.new(template, date: date, today: today, selected_date: selected_date).render
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -3,54 +3,95 @@
3
3
  module StimulusPlumbers
4
4
  module Components
5
5
  class Calendar < Plumber::Base
6
- STIMULUS_CONTROLLER = "calendar-month"
7
- OBSERVER_STIMULUS_CONTROLLER = "calendar-month-observer"
8
- STIMULUS_DATA = {
9
- controller: "#{STIMULUS_CONTROLLER} #{OBSERVER_STIMULUS_CONTROLLER}",
10
- action: [
11
- "click->#{OBSERVER_STIMULUS_CONTROLLER}#onSelect",
12
- "#{OBSERVER_STIMULUS_CONTROLLER}:selected->#{STIMULUS_CONTROLLER}#onSelect"
13
- ].join(" ")
14
- }.freeze
15
-
16
- def month(**kwargs)
17
- template.content_tag(:div, role: "grid", **month_html_options(kwargs)) do
18
- template.safe_join([template.tag.div(**dow_options), template.tag.div(**dom_options)])
6
+ MONTH_STIMULUS_CONTROLLER = "calendar-month"
7
+ YEAR_STIMULUS_CONTROLLER = "calendar-year"
8
+ DECADE_STIMULUS_CONTROLLER = "calendar-decade"
9
+ OBSERVER_STIMULUS_CONTROLLER = "calendar-observer"
10
+ STIMULUS_ACTION = "click->#{OBSERVER_STIMULUS_CONTROLLER}#onSelect".freeze
11
+
12
+ def render(**kwargs)
13
+ html_options = merge_html_options(
14
+ theme.resolve(:calendar),
15
+ kwargs,
16
+ {
17
+ data: {
18
+ controller: "#{MONTH_STIMULUS_CONTROLLER} #{OBSERVER_STIMULUS_CONTROLLER}",
19
+ action: STIMULUS_ACTION
20
+ }
21
+ }
22
+ )
23
+ template.content_tag(:div, **html_options, role: "grid") do
24
+ template.safe_join([month, year, decade])
25
+ end
26
+ end
27
+
28
+ def month
29
+ template.content_tag(:div, **month_options) do
30
+ template.safe_join(
31
+ [
32
+ template.tag.div(**dow_options),
33
+ template.tag.div(**dom_options)
34
+ ]
35
+ )
19
36
  end
20
37
  end
21
38
 
39
+ def year
40
+ template.tag.div(**year_options)
41
+ end
42
+
43
+ def decade
44
+ template.tag.div(**decade_options)
45
+ end
46
+
22
47
  private
23
48
 
24
- def month_html_options(kwargs)
49
+ def month_options
25
50
  merge_html_options(
26
51
  {
27
- classes: theme.resolve(:calendar).fetch(:classes, ""),
28
- data: month_stimulus_data
29
- },
30
- kwargs
52
+ data: {
53
+ "#{MONTH_STIMULUS_CONTROLLER}-row-class": theme.resolve(:calendar_row).fetch(:classes, ""),
54
+ "#{MONTH_STIMULUS_CONTROLLER}-day-of-month-class": theme.resolve(:calendar_day).fetch(:classes, "")
55
+ }
56
+ }
31
57
  )
32
58
  end
33
59
 
34
- def month_stimulus_data
35
- STIMULUS_DATA.merge(
36
- calendar_month_week_class: theme.resolve(:calendar_week).fetch(:classes, ""),
37
- calendar_month_day_of_month_class: theme.resolve(:calendar_day).fetch(:classes, "")
38
- ).compact_blank
39
- end
40
-
41
60
  def dow_options
42
61
  merge_html_options(
43
- { classes: theme.resolve(:calendar_days_of_week).fetch(:classes, "") },
44
- { data: { "#{STIMULUS_CONTROLLER}-target": "daysOfWeek" } }
62
+ theme.resolve(:calendar_days_of_week),
63
+ { data: { "#{MONTH_STIMULUS_CONTROLLER}-target": "daysOfWeek" } }
45
64
  )
46
65
  end
47
66
 
48
67
  def dom_options
49
68
  merge_html_options(
50
- { classes: theme.resolve(:calendar_days_of_month).fetch(:classes, "") },
69
+ theme.resolve(:calendar_days_of_month),
51
70
  {
52
71
  role: "rowgroup",
53
- data: { "#{STIMULUS_CONTROLLER}-target": "daysOfMonth" }
72
+ data: { "#{MONTH_STIMULUS_CONTROLLER}-target": "daysOfMonth" }
73
+ }
74
+ )
75
+ end
76
+
77
+ def year_options
78
+ merge_html_options(
79
+ { hidden: true, data: { controller: YEAR_STIMULUS_CONTROLLER } },
80
+ {
81
+ data: {
82
+ "#{YEAR_STIMULUS_CONTROLLER}-month-class": theme.resolve(:calendar_month).fetch(:classes, "")
83
+ }
84
+ }
85
+ )
86
+ end
87
+
88
+ def decade_options
89
+ merge_html_options(
90
+ { hidden: true, data: { controller: DECADE_STIMULUS_CONTROLLER } },
91
+ {
92
+ data: {
93
+ "#{DECADE_STIMULUS_CONTROLLER}-year-class": theme.resolve(:calendar_year).fetch(:classes, "")
94
+ }
54
95
  }
55
96
  )
56
97
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Card
6
+ class Slots < Plumber::Slots
7
+ slot :icon, :title
8
+
9
+ def with_body(&block)
10
+ raise ArgumentError, "card.body requires a block" unless block
11
+
12
+ set_slot(:body, block)
13
+ nil
14
+ end
15
+
16
+ # Defined manually (not via `slot` DSL) because it requires a named `url:` keyword with validation.
17
+ def with_action(value = nil, url: nil, &block)
18
+ raise ArgumentError, "card.action requires content (string or block) when url: is given" if url && value.nil? && !block
19
+
20
+ set_slot(:action, block || value, url ? { url: url } : {})
21
+ nil
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -3,31 +3,71 @@
3
3
  module StimulusPlumbers
4
4
  module Components
5
5
  class Card < Plumber::Base
6
- def render(...)
7
- render_card(...)
8
- end
6
+ def render(variant: :tertiary, title_tag: :h2, **kwargs, &block)
7
+ slots = Card::Slots.new
8
+ yield slots if block_given?
9
9
 
10
- def section(...)
11
- Card::Section.new(template).render(...)
10
+ html_options = merge_html_options(theme.resolve(:card, variant: variant), kwargs)
11
+ template.content_tag(:div, **html_options) do
12
+ template.safe_join(
13
+ [
14
+ render_header(slots, title_tag),
15
+ render_body(slots),
16
+ render_action(slots)
17
+ ].compact
18
+ )
19
+ end
12
20
  end
13
21
 
14
22
  private
15
23
 
16
- def render_card(title: nil, title_tag: :h2, **kwargs, &block)
17
- html_options = merge_html_options(
18
- { classes: theme.resolve(:card).fetch(:classes, "") },
19
- kwargs
20
- )
24
+ def render_header(slots, title_tag)
25
+ icon = slots.resolve(:icon) { |value| render_icon_slot(value) }
26
+ title = slots.resolve(:title)
27
+ return unless icon || title
21
28
 
22
- template.content_tag(:div, **html_options) do
29
+ template.content_tag(:div, **merge_html_options(theme.resolve(:card_header))) do
23
30
  template.safe_join(
24
31
  [
25
- (template.content_tag(title_tag, title) if title.present?),
26
- template.capture(&block)
27
- ]
32
+ icon,
33
+ (template.content_tag(title_tag, title, **merge_html_options(theme.resolve(:card_title))) if title)
34
+ ].compact
28
35
  )
29
36
  end
30
37
  end
38
+
39
+ def render_body(slots)
40
+ content = slots.resolve(:body)
41
+ return unless content
42
+
43
+ template.content_tag(:div, **merge_html_options(theme.resolve(:card_body))) do
44
+ content
45
+ end
46
+ end
47
+
48
+ def render_icon_slot(value)
49
+ return value unless value.is_a?(Symbol) || (value.is_a?(String) && !value.html_safe?)
50
+
51
+ Components::Icon.new(template).render(
52
+ name: value,
53
+ classes: theme.resolve(:card_icon).fetch(:classes, ""),
54
+ aria: { hidden: "true" }
55
+ )
56
+ end
57
+
58
+ def render_action(slots)
59
+ content = slots.resolve(:action)
60
+ return unless content
61
+
62
+ url = slots.options_for(:action)[:url]
63
+ template.content_tag(:div, **merge_html_options(theme.resolve(:card_action))) do
64
+ if url.present?
65
+ template.content_tag(:a, href: url) { content }
66
+ else
67
+ template.content_tag(:button, type: "button") { content }
68
+ end
69
+ end
70
+ end
31
71
  end
32
72
  end
33
73
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Combobox
6
+ # Yielded to `Combobox#render`: selects a variant renderer, then exposes its
7
+ # `metadata` (trigger/wrapper wiring) and renders its panel body.
8
+ class Builder < Plumber::Slots
9
+ def dropdown(**options) = select(Dropdown, options)
10
+ def typeahead(**options) = select(Typeahead, options)
11
+ def date(**options) = select(Date, options)
12
+ def time(**options) = select(Time, options)
13
+
14
+ def selected? = @slots.key?(:variant)
15
+ def renderer = selection&.fetch(:renderer)
16
+ def options = selection ? selection[:options] : {}
17
+ def metadata = renderer ? renderer::Metadata : DefaultMetadata
18
+
19
+ def render_panel(template, panel_attrs:)
20
+ renderer&.new(template)&.render(panel_attrs: panel_attrs, **options)
21
+ end
22
+
23
+ # Metadata used when no variant is selected.
24
+ module DefaultMetadata
25
+ module_function
26
+
27
+ def haspopup = "dialog"
28
+ def popup_id_for(panel_id) = panel_id
29
+ def trigger_icon = nil
30
+ def trigger_options = {}
31
+ def stimulus_data(_panel_id, _options) = {}
32
+ end
33
+
34
+ private
35
+
36
+ def select(renderer, options)
37
+ set_slot(:variant, { renderer: renderer, options: options })
38
+ nil
39
+ end
40
+
41
+ def selection = resolve(:variant)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Combobox
6
+ class Date
7
+ class Navigation < Plumber::Base
8
+ def render(step:, stimulus_controller:, view: "month", date: ::Date.today, **kwargs)
9
+ html_options = merge_html_options(
10
+ theme.resolve(:combobox_date_navigation),
11
+ kwargs,
12
+ { aria: { label: "Date picker navigation" } }
13
+ )
14
+
15
+ template.content_tag(:nav, **html_options) do
16
+ template.safe_join(navigators(stimulus_controller, step, view, date))
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def navigators(stimulus_controller, step, view, date)
23
+ [
24
+ navigator(stimulus_controller, target: "previous", icon: "arrow-left", label: prev_label(step)),
25
+ view_title_navigator(stimulus_controller, view, date),
26
+ navigator(stimulus_controller, target: "next", icon: "arrow-right", label: next_label(step))
27
+ ]
28
+ end
29
+
30
+ def navigator(stimulus_controller, target:, label:, icon: nil)
31
+ opts = {
32
+ aria: { label: label },
33
+ data: { "#{stimulus_controller}-target" => target }
34
+ }
35
+ opts[:icon] = icon if icon
36
+ Navigator.new(template).render(**opts)
37
+ end
38
+
39
+ def view_title_navigator(stimulus_controller, view, date)
40
+ Navigator.new(template).render(
41
+ data: {
42
+ "#{stimulus_controller}-target" => "viewTitle",
43
+ action: "click->#{stimulus_controller}#zoomOut"
44
+ }
45
+ ) { view_title_label(view, date) }
46
+ end
47
+
48
+ def view_title_label(view, date)
49
+ case view
50
+ when "year" then date.year.to_s
51
+ when "decade" then decade_label(date)
52
+ else I18n.l(date, format: "%B %Y")
53
+ end
54
+ end
55
+
56
+ def decade_label(date)
57
+ start = (date.year / 10) * 10
58
+ "#{start}–#{start + 9}"
59
+ end
60
+
61
+ def prev_label(step)
62
+ "Previous #{step.to_s.titleize}"
63
+ end
64
+
65
+ def next_label(step)
66
+ "Next #{step.to_s.titleize}"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end