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,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Calendar
6
+ class Turbo
7
+ class DaysOfMonth < Plumber::Base
8
+ DAYS_IN_WEEK = 7
9
+
10
+ attr_reader :date, :today, :selectable, :selected_date, :show_other_months
11
+
12
+ def initialize(
13
+ template,
14
+ date: Date.today,
15
+ today: Date.today,
16
+ selectable: false,
17
+ selected_date: nil,
18
+ show_other_months: false
19
+ )
20
+ super(template)
21
+ @date = date
22
+ @today = today
23
+ @selectable = selectable
24
+ @selected_date = selected_date
25
+ @show_other_months = show_other_months
26
+ end
27
+
28
+ def render(...)
29
+ render_days_of_month(...)
30
+ end
31
+
32
+ private
33
+
34
+ def render_days_of_month(**html_options)
35
+ html_options = merge_html_options(
36
+ theme.resolve(:calendar_days_of_month),
37
+ html_options
38
+ )
39
+ template.content_tag(:div, **html_options, role: "rowgroup") { weeks_of_month }
40
+ end
41
+
42
+ def weeks_of_month
43
+ week_options = merge_html_options(
44
+ theme.resolve(:calendar_row),
45
+ { role: "row" }
46
+ )
47
+ template.safe_join(
48
+ build_days.each_slice(DAYS_IN_WEEK).map do |days|
49
+ template.content_tag(:div, **week_options) { days_in_row(days) }
50
+ end
51
+ )
52
+ end
53
+
54
+ def focus_day
55
+ @focus_day ||= if selected_date_in_current_month?
56
+ selected_date
57
+ elsif today_in_current_month?
58
+ today
59
+ else
60
+ date.beginning_of_month
61
+ end
62
+ end
63
+
64
+ def build_days
65
+ first = date.beginning_of_month
66
+ last = date.end_of_month
67
+ current_days = first.upto(last).to_a
68
+ prev_filler_days(first) + current_days + next_filler_days(last, current_days.length)
69
+ end
70
+
71
+ def prev_filler_days(first_day_of_month)
72
+ week_start = first_day_of_month - first_day_of_month.wday
73
+ week_start.upto(first_day_of_month - 1).to_a
74
+ end
75
+
76
+ def next_filler_days(last_day_of_month, days_in_month)
77
+ week_start_offset = last_day_of_month.beginning_of_month.wday
78
+ total = week_start_offset + days_in_month
79
+ next_count = (DAYS_IN_WEEK - (total % DAYS_IN_WEEK)) % DAYS_IN_WEEK
80
+ next_count.positive? ? (last_day_of_month + 1).upto(last_day_of_month + next_count).to_a : []
81
+ end
82
+
83
+ def selected_date_in_current_month?
84
+ selected_date&.month == date.month && selected_date&.year == date.year
85
+ end
86
+
87
+ def today_in_current_month?
88
+ today.month == date.month && today.year == date.year
89
+ end
90
+
91
+ def days_in_row(days)
92
+ template.safe_join(
93
+ days.map do |day|
94
+ if day.month == date.month
95
+ current_month_day_cell(day)
96
+ elsif show_other_months
97
+ other_month_day_cell(day)
98
+ else
99
+ hidden_day_cell(day)
100
+ end
101
+ end
102
+ )
103
+ end
104
+
105
+ def hidden_day_cell(date)
106
+ html_options = merge_html_options(
107
+ theme.resolve(:calendar_day),
108
+ { role: "gridcell", tabindex: -1, aria: { hidden: "true" } }
109
+ )
110
+ template.content_tag(:span, **html_options) do
111
+ template.content_tag(:time, nil, datetime: date.iso8601)
112
+ end
113
+ end
114
+
115
+ def current_month_day_cell(date)
116
+ tag = selectable ? :button : :span
117
+ template.content_tag(tag, **day_cell_html_options(date)) do
118
+ template.content_tag(:time, date.day.to_s, datetime: date.iso8601)
119
+ end
120
+ end
121
+
122
+ def day_cell_html_options(date)
123
+ is_today = date == today
124
+ is_selected = selected_date && date == selected_date
125
+ merge_html_options(
126
+ theme.resolve(:calendar_day),
127
+ {
128
+ role: "gridcell",
129
+ tabindex: date == focus_day ? 0 : -1,
130
+ aria: {
131
+ current: is_today ? "date" : nil,
132
+ selected: if selectable then is_selected ? "true" : "false" end
133
+ }
134
+ }
135
+ )
136
+ end
137
+
138
+ def other_month_day_cell(date)
139
+ options = merge_html_options(
140
+ theme.resolve(:calendar_day, outside: true),
141
+ { role: "gridcell", tabindex: -1, aria: { disabled: "true", selected: "false" } }
142
+ )
143
+ template.content_tag(:span, **options) do
144
+ template.content_tag(:time, date.day.to_s, datetime: date.iso8601)
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -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,51 +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: "click->#{OBSERVER_STIMULUS_CONTROLLER}#select"
11
- }.freeze
12
-
13
- def month(**kwargs)
14
- template.content_tag(:div, role: "grid", **month_html_options(kwargs)) do
15
- 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
+ )
16
36
  end
17
37
  end
18
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
+
19
47
  private
20
48
 
21
- def month_html_options(kwargs)
49
+ def month_options
22
50
  merge_html_options(
23
51
  {
24
- classes: theme.resolve(:calendar).fetch(:classes, ""),
25
- data: month_stimulus_data
26
- },
27
- 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
+ }
28
57
  )
29
58
  end
30
59
 
31
- def month_stimulus_data
32
- STIMULUS_DATA.merge(
33
- calendar_month_week_class: theme.resolve(:calendar_week).fetch(:classes, ""),
34
- calendar_month_day_of_month_class: theme.resolve(:calendar_day).fetch(:classes, "")
35
- ).compact_blank
36
- end
37
-
38
60
  def dow_options
39
61
  merge_html_options(
40
- { classes: theme.resolve(:calendar_days_of_week).fetch(:classes, "") },
41
- { data: { "#{STIMULUS_CONTROLLER}-target": "daysOfWeek" } }
62
+ theme.resolve(:calendar_days_of_week),
63
+ { data: { "#{MONTH_STIMULUS_CONTROLLER}-target": "daysOfWeek" } }
42
64
  )
43
65
  end
44
66
 
45
67
  def dom_options
46
68
  merge_html_options(
47
- { classes: theme.resolve(:calendar_days_of_month).fetch(:classes, "") },
69
+ theme.resolve(:calendar_days_of_month),
48
70
  {
49
71
  role: "rowgroup",
50
- 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
+ }
51
95
  }
52
96
  )
53
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,24 +3,70 @@
3
3
  module StimulusPlumbers
4
4
  module Components
5
5
  class Card < Plumber::Base
6
- def render(title: nil, title_tag: :h2, **kwargs, &block)
7
- html_options = merge_html_options(
8
- { classes: theme.resolve(:card).fetch(:classes, "") },
9
- kwargs
10
- )
6
+ def render(variant: :tertiary, title_tag: :h2, **kwargs, &block)
7
+ slots = Card::Slots.new
8
+ yield slots if block_given?
11
9
 
10
+ html_options = merge_html_options(theme.resolve(:card, variant: variant), kwargs)
12
11
  template.content_tag(:div, **html_options) do
13
12
  template.safe_join(
14
13
  [
15
- (template.content_tag(title_tag, title) if title.present?),
16
- template.capture(&block)
17
- ]
14
+ render_header(slots, title_tag),
15
+ render_body(slots),
16
+ render_action(slots)
17
+ ].compact
18
+ )
19
+ end
20
+ end
21
+
22
+ private
23
+
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
28
+
29
+ template.content_tag(:div, **merge_html_options(theme.resolve(:card_header))) do
30
+ template.safe_join(
31
+ [
32
+ icon,
33
+ (template.content_tag(title_tag, title, **merge_html_options(theme.resolve(:card_title))) if title)
34
+ ].compact
18
35
  )
19
36
  end
20
37
  end
21
38
 
22
- def section(title: nil, **kwargs, &block)
23
- Card::Section.new(template).render(title: title, **kwargs, &block)
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
24
70
  end
25
71
  end
26
72
  end