sdr_view_components 0.1.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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +75 -0
  4. data/Rakefile +15 -0
  5. data/app/assets/stylesheets/styles.scss +118 -0
  6. data/app/components/base_component.rb +31 -0
  7. data/app/components/component_support/button_support.rb +14 -0
  8. data/app/components/component_support/css_classes.rb +16 -0
  9. data/app/components/component_support/file_hierarchy.rb +22 -0
  10. data/app/components/sdr_view_components/elements/alert_component.html.erb +14 -0
  11. data/app/components/sdr_view_components/elements/alert_component.rb +65 -0
  12. data/app/components/sdr_view_components/elements/banner_component.html.erb +20 -0
  13. data/app/components/sdr_view_components/elements/banner_component.rb +41 -0
  14. data/app/components/sdr_view_components/elements/breadcrumb_component.html.erb +7 -0
  15. data/app/components/sdr_view_components/elements/breadcrumb_component.rb +28 -0
  16. data/app/components/sdr_view_components/elements/breadcrumb_nav_component.html.erb +10 -0
  17. data/app/components/sdr_view_components/elements/breadcrumb_nav_component.rb +24 -0
  18. data/app/components/sdr_view_components/elements/button_component.rb +31 -0
  19. data/app/components/sdr_view_components/elements/button_link_component.rb +29 -0
  20. data/app/components/sdr_view_components/elements/navigation/dropdown_menu_component.html.erb +13 -0
  21. data/app/components/sdr_view_components/elements/navigation/dropdown_menu_component.rb +19 -0
  22. data/app/components/sdr_view_components/elements/navigation/nav_item_component.rb +25 -0
  23. data/app/components/sdr_view_components/elements/skip_links_component.html.erb +5 -0
  24. data/app/components/sdr_view_components/elements/skip_links_component.rb +8 -0
  25. data/app/components/sdr_view_components/elements/toast_component.html.erb +40 -0
  26. data/app/components/sdr_view_components/elements/toast_component.rb +35 -0
  27. data/app/components/sdr_view_components/elements/tooltip_component.html.erb +11 -0
  28. data/app/components/sdr_view_components/elements/tooltip_component.rb +39 -0
  29. data/app/components/sdr_view_components/forms/basic_checkbox_component.rb +22 -0
  30. data/app/components/sdr_view_components/forms/button_component.rb +42 -0
  31. data/app/components/sdr_view_components/forms/checkbox_component.html.erb +6 -0
  32. data/app/components/sdr_view_components/forms/checkbox_component.rb +23 -0
  33. data/app/components/sdr_view_components/forms/field_component.html.erb +6 -0
  34. data/app/components/sdr_view_components/forms/field_component.rb +74 -0
  35. data/app/components/sdr_view_components/forms/help_text_component.html.erb +3 -0
  36. data/app/components/sdr_view_components/forms/help_text_component.rb +23 -0
  37. data/app/components/sdr_view_components/forms/invalid_feedback_component.rb +43 -0
  38. data/app/components/sdr_view_components/forms/invalid_feedback_support.rb +21 -0
  39. data/app/components/sdr_view_components/forms/label_component.html.erb +6 -0
  40. data/app/components/sdr_view_components/forms/label_component.rb +33 -0
  41. data/app/components/sdr_view_components/forms/submit_component.html.erb +3 -0
  42. data/app/components/sdr_view_components/forms/submit_component.rb +25 -0
  43. data/app/components/sdr_view_components/forms/toggle_component.html.erb +10 -0
  44. data/app/components/sdr_view_components/forms/toggle_component.rb +24 -0
  45. data/app/components/sdr_view_components/forms/toggle_option_component.html.erb +2 -0
  46. data/app/components/sdr_view_components/forms/toggle_option_component.rb +26 -0
  47. data/app/components/sdr_view_components/structure/footer_component.html.erb +141 -0
  48. data/app/components/sdr_view_components/structure/footer_component.rb +9 -0
  49. data/app/components/sdr_view_components/structure/header_component.html.erb +65 -0
  50. data/app/components/sdr_view_components/structure/header_component.rb +63 -0
  51. data/app/components/sdr_view_components/structure/style_override_dark_component.html.erb +9 -0
  52. data/app/components/sdr_view_components/structure/style_override_dark_component.rb +22 -0
  53. data/app/components/sdr_view_components/structure/style_override_light_component.html.erb +6 -0
  54. data/app/components/sdr_view_components/structure/style_override_light_component.rb +9 -0
  55. data/config/routes.rb +4 -0
  56. data/lib/sdr_view_components/engine.rb +25 -0
  57. data/lib/sdr_view_components/version.rb +5 -0
  58. data/lib/sdr_view_components.rb +12 -0
  59. metadata +141 -0
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrViewComponents
4
+ module Elements
5
+ # Skip links for accessibility
6
+ class SkipLinksComponent < BaseComponent; end
7
+ end
8
+ end
@@ -0,0 +1,40 @@
1
+ <style>
2
+ .toast:not(.show) {
3
+ display: block;
4
+ }
5
+ </style>
6
+ <div class="toast-container position-relative bottom-0 end-0 p-3">
7
+ <div
8
+ class="toast align-items-center"
9
+ role="alert"
10
+ aria-live="assertive"
11
+ aria-atomic="true"
12
+ >
13
+ <div class="<%= toast_class %>">
14
+ <div class="d-flex">
15
+ <div
16
+ class="bi bi-exclamation-circle-fill fs-3 me-3 align-self-center d-flex justify-content-center"
17
+ ></div>
18
+ <div>
19
+ <div class="fw-semibold"><%= title %> </div>
20
+ <div><%= text %></div>
21
+ </div>
22
+ <div class="me-2 m-auto">
23
+ <% if close_text.present? %>
24
+ <button
25
+ type="button"
26
+ class="btn btn-text text-uppercase text-white"
27
+ >
28
+ <%= close_text %>
29
+ </button>
30
+ <% end %>
31
+ <button
32
+ type="button"
33
+ class="btn btn-close btn-close-white"
34
+ aria-label="Close"
35
+ ></button>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ </div>
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrViewComponents
4
+ module Elements
5
+ # Component for rendering a toast element.
6
+ class ToastComponent < BaseComponent
7
+ def initialize(title:, text:, close_text: nil, variant: :black)
8
+ @title = title
9
+ @text = text
10
+ @close_text = close_text
11
+ @variant = variant
12
+ super()
13
+ end
14
+
15
+ attr_reader :title, :text, :close_text, :variant
16
+
17
+ def toast_class
18
+ merge_classes([background_color], %w[toast-body text-white])
19
+ end
20
+
21
+ def background_color
22
+ case variant
23
+ when :red
24
+ 'bg-stanford-cardinal'
25
+ when :green
26
+ 'bg-stanford-digital-green'
27
+ when :poppy
28
+ 'bg-poppy-dark'
29
+ else
30
+ 'bg-stanford-black'
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,11 @@
1
+ <%= tag.a(
2
+ role: 'button',
3
+ tabindex: 0,
4
+ class: 'px-2 tooltip-info',
5
+ aria: {
6
+ label: "More information for #{target_label}"
7
+ },
8
+ data:
9
+ ) do %>
10
+ <%= helpers.info_icon(fill: true) %>
11
+ <% end %>
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrViewComponents
4
+ module Elements
5
+ # Component for rendering a tooltip.
6
+ class TooltipComponent < BaseComponent
7
+ def initialize(target_label:, tooltip: nil)
8
+ @target_label = target_label
9
+ @tooltip = tooltip
10
+ super()
11
+ end
12
+
13
+ private
14
+
15
+ attr_reader :tooltip, :target_label
16
+
17
+ def render?
18
+ tooltip.present?
19
+ end
20
+
21
+ def data # rubocop:disable Metrics/MethodLength
22
+ {
23
+ bs_html: true,
24
+ bs_toggle: 'tooltip',
25
+ bs_title: tooltip,
26
+ bs_trigger: 'focus',
27
+ tooltips_target: 'icon'
28
+ }.tap do |data|
29
+ if Settings.ahoy.tooltip
30
+ data[:controller] = 'ahoy-tooltip'
31
+ data[:ahoy_tooltip_label_value] = target_label
32
+ # Note that this is only tracking tooltip when shown by clicking, not when shown by focus.
33
+ data[:action] = 'click->ahoy-tooltip#track'
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrViewComponents
4
+ module Forms
5
+ # Component for form checkbox field
6
+ class BasicCheckboxComponent < BaseComponent
7
+ def initialize(form:, field_name:, data: nil, **args)
8
+ @form = form
9
+ @field_name = field_name
10
+ @args = args
11
+ @data = data
12
+ super()
13
+ end
14
+
15
+ attr_reader :args, :data, :form, :field_name, :value
16
+
17
+ def call
18
+ form.check_box field_name, data:, **args
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrViewComponents
4
+ module Forms
5
+ # Component for a button that is wrapped in a form
6
+ class ButtonComponent < BaseComponent
7
+ def initialize(link:, label: nil, variant: :primary, classes: [], method: :get, confirm: nil, # rubocop:disable Metrics/ParameterLists
8
+ top: true, data: {})
9
+ @link = link
10
+ @label = label
11
+ @variant = variant
12
+ @classes = classes
13
+ @method = method
14
+ @confirm = confirm
15
+ @top = top
16
+ @data = data
17
+ super()
18
+ end
19
+
20
+ attr_reader :link
21
+
22
+ def call
23
+ button_to(link, method: @method,
24
+ class: ComponentSupport::ButtonSupport.classes(variant: @variant, classes:),
25
+ form: { data: }) do
26
+ @label || content
27
+ end
28
+ end
29
+
30
+ def classes
31
+ merge_classes(@classes)
32
+ end
33
+
34
+ def data
35
+ @data.tap do |data|
36
+ data[:turbo_frame] = '_top' if @top
37
+ data[:turbo_confirm] = @confirm if @confirm
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,6 @@
1
+ <%= tag.div container_args do %>
2
+ <%= render SdrViewComponents::Forms::BasicCheckboxComponent.new(form:, field_name:, data:, **input_args) %>
3
+ <%= render SdrViewComponents::Forms::LabelComponent.new(form:, field_name:, **label_args) %>
4
+ <%= render SdrViewComponents::Forms::HelpTextComponent.new(**help_text_args) %>
5
+ <%= render SdrViewComponents::Forms::InvalidFeedbackComponent.new(form:, field_name:) %>
6
+ <% end %>
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrViewComponents
4
+ module Forms
5
+ # Component for form checkbox field
6
+ class CheckboxComponent < FieldComponent
7
+ def initialize(**args)
8
+ args[:container_class] = merge_classes('form-check', args[:container_class])
9
+ args[:input_class] = merge_classes('form-check-input', args[:input_class])
10
+ args[:label_default_class] = merge_classes('form-check-label', args[:input_class])
11
+ super
12
+ end
13
+
14
+ attr_reader :value
15
+
16
+ # The component must implement a `default_component` method in order
17
+ # to render in the component slot of the FieldComponent.
18
+ def default_component
19
+ render SdrViewComponents::Forms::BasicCheckboxComponent.new(form:, field_name:, data:, **input_args)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,6 @@
1
+ <%= tag.div container_args do %>
2
+ <%= component %>
3
+ <%= render SdrViewComponents::Forms::LabelComponent.new(form:, field_name:, **label_args) %>
4
+ <%= render SdrViewComponents::Forms::HelpTextComponent.new(**help_text_args) %>
5
+ <%= render SdrViewComponents::Forms::InvalidFeedbackComponent.new(form:, field_name:) %>
6
+ <% end %>
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrViewComponents
4
+ module Forms
5
+ # Base component for all form fields.
6
+ class FieldComponent < BaseComponent
7
+ renders_one :component
8
+
9
+ def initialize(**args)
10
+ @args = args
11
+ super()
12
+ end
13
+
14
+ attr_reader :args
15
+
16
+ def input_args
17
+ args_for(args:, prefix: 'input_').merge({ aria: field_aria, data: })
18
+ end
19
+
20
+ def container_args
21
+ args_for(args:, prefix: 'container_')
22
+ end
23
+
24
+ def data
25
+ @data ||= args.delete(:data) || {}
26
+ end
27
+
28
+ def error_aria
29
+ InvalidFeedbackSupport.arias_for(field_name:, form:).tap do |arias|
30
+ arias[:describedby] = merge_actions(arias[:describedby], help_text_id) if help_text_args[:text].present?
31
+ end
32
+ end
33
+
34
+ def error_classes
35
+ merge_classes(args.fetch(:error_classes, []))
36
+ end
37
+
38
+ def field_aria
39
+ error_aria.tap do |arias|
40
+ # Set aria-required if we want to indicate required, but the field
41
+ # does not actually have a required attribute
42
+ #
43
+ # This is used for collection/work forms where we do server-side
44
+ # validation and don't want to block form submission on empty fields
45
+ arias[:required] = true if @mark_required
46
+ end
47
+ end
48
+
49
+ def field_name
50
+ @field_name ||= args.delete(:field_name)
51
+ end
52
+
53
+ def form
54
+ @form ||= args.delete(:form)
55
+ end
56
+
57
+ def help_text_args
58
+ args_for(args:, prefix: 'help_').merge({
59
+ id: help_text_id
60
+ })
61
+ end
62
+
63
+ def help_text_id
64
+ @help_text_id ||= form.field_id(field_name, 'help')
65
+ end
66
+
67
+ def label_args
68
+ args_for(args:, prefix: 'label_')
69
+ end
70
+
71
+ delegate :id, to: :form, prefix: true
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,3 @@
1
+ <div>
2
+ <%= tag.p id:, class: 'form-text' do %><%= text || content %><% end %>
3
+ </div>
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Component for rendering help text for form fields.
4
+ module SdrViewComponents
5
+ module Forms
6
+ # Component for rendering help text for form fields.
7
+ class HelpTextComponent < BaseComponent
8
+ # this component can take plain text via 'help_text' or a block (which can contain html)
9
+ # it will render the help_text if provided, else it will render the block content
10
+ def initialize(id:, text: nil)
11
+ @text = text
12
+ @id = id
13
+ super()
14
+ end
15
+
16
+ attr_reader :text, :id
17
+
18
+ def render?
19
+ text.present? || content.present?
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrViewComponents
4
+ module Forms
5
+ # Component for rendering invalid feedback for a form field.
6
+ class InvalidFeedbackComponent < BaseComponent
7
+ def initialize(field_name:, form:, classes: [], data: {})
8
+ @field_name = field_name
9
+ @form = form
10
+ @classes = classes
11
+ @data = data
12
+ super()
13
+ end
14
+
15
+ attr_reader :field_name, :form, :data
16
+
17
+ def call
18
+ tag.div(class: classes, id:, data:) do
19
+ errors.join(', ')
20
+ end
21
+ end
22
+
23
+ def render?
24
+ field_name.present? && errors.present?
25
+ end
26
+
27
+ private
28
+
29
+ def id
30
+ InvalidFeedbackSupport.id_for(field_name:, form:)
31
+ end
32
+
33
+ def errors
34
+ @errors ||= form.object&.errors&.[](field_name)
35
+ end
36
+
37
+ def classes
38
+ # Adding is-invalid to trigger the tab error.
39
+ merge_classes(%w[invalid-feedback is-invalid d-block], @classes)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrViewComponents
4
+ module Forms
5
+ # Methods to support invalid feedback for form fields.
6
+ class InvalidFeedbackSupport
7
+ def self.id_for(field_name:, form:)
8
+ form.field_id(field_name, 'error')
9
+ end
10
+
11
+ def self.arias_for(field_name:, form:)
12
+ return {} if field_name.nil? || form.object&.errors&.[](field_name).blank?
13
+
14
+ {
15
+ invalid: true,
16
+ describedby: id_for(field_name:, form:)
17
+ }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,6 @@
1
+ <%= form.label field_name, class: classes do %>
2
+ <%= content || text %>
3
+ <% end %>
4
+ <%# This is outside the label so that clicking on it doesn't toggle some inputs like checkboxes. %>
5
+ <%= render SdrViewComponents::Elements::TooltipComponent.new(target_label: text, tooltip:) %>
6
+ <%= tag.span { caption } if caption %>
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrViewComponents
4
+ module Forms
5
+ # Component for rendering a form label.
6
+ class LabelComponent < BaseComponent
7
+ def initialize(form:, field_name:, text: nil, default_class: 'form-label', hidden: false, # rubocop:disable Metrics/ParameterLists
8
+ classes: [], tooltip: nil, caption: nil)
9
+ @form = form
10
+ @text = text
11
+ @field_name = field_name
12
+ @hidden = hidden
13
+ @default_class = default_class
14
+ @classes = classes
15
+ @tooltip = tooltip
16
+ @caption = caption
17
+ super()
18
+ end
19
+
20
+ attr_reader :field_name, :form, :tooltip, :caption
21
+
22
+ def text
23
+ return field_name if @text.blank?
24
+
25
+ @text
26
+ end
27
+
28
+ def classes
29
+ merge_classes(@default_class, @classes, @hidden ? 'visually-hidden' : nil)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ <%= tag.button type: 'submit', form: form_id, class: classes, name: 'commit', value:, **options do %>
2
+ <%= label || content %>
3
+ <% end %>
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrViewComponents
4
+ module Forms
5
+ # Component for a form submit button
6
+ class SubmitComponent < BaseComponent
7
+ def initialize(label: nil, value: nil, form_id: nil, variant: :primary, classes: [], **options) # rubocop:disable Metrics/ParameterLists
8
+ # Either provide label OR value and content
9
+ @form_id = form_id
10
+ @label = label
11
+ @variant = variant
12
+ @options = options
13
+ @classes = classes
14
+ @value = value || label
15
+ super()
16
+ end
17
+
18
+ attr_reader :form, :label, :options, :form_id, :value
19
+
20
+ def classes
21
+ ComponentSupport::ButtonSupport.classes(variant: @variant, classes: @classes)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,10 @@
1
+ <%= tag.div **container_args do %>
2
+ <div class="d-flex"> <%# Aligns label and tooltip %>
3
+ <%= render SdrViewComponents::Forms::LabelComponent.new(form:, field_name:, **label_args) %>
4
+ </div>
5
+ <div class="btn-group btn-group-toggle" role="group">
6
+ <%= left_toggle_option %>
7
+ <%= right_toggle_option %>
8
+ </div>
9
+ <%= render SdrViewComponents::Forms::InvalidFeedbackComponent.new(form:, field_name:) %>
10
+ <% end %>
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrViewComponents
4
+ module Forms
5
+ # Component for a toggle-like radio button group field
6
+ class ToggleComponent < FieldComponent
7
+ renders_one :left_toggle_option, lambda { |**args|
8
+ Forms::ToggleOptionComponent.new(position: :left, **args)
9
+ }
10
+ renders_one :right_toggle_option, lambda { |**args|
11
+ Forms::ToggleOptionComponent.new(position: :right, **args)
12
+ }
13
+
14
+ def initialize(form:, field_name:, **args)
15
+ @form = form
16
+ @field_name = field_name
17
+ args[:label_classes] = merge_classes('d-block', args[:label_classes])
18
+ super
19
+ end
20
+
21
+ attr_reader :form, :field_name
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,2 @@
1
+ <%= form.radio_button field_name, value, type: 'radio', class: 'btn-check', data: %>
2
+ <%= form.label field_name, label, value:, class: label_classes %>
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrViewComponents
4
+ module Forms
5
+ # Component for a toggle option
6
+ class ToggleOptionComponent < BaseComponent
7
+ def initialize(form:, field_name:, label:, value:, data: {}, position: :left) # rubocop:disable Metrics/ParameterLists
8
+ raise ArgumentError, 'position must be :left or :right' unless %i[left right].include?(position)
9
+
10
+ @form = form
11
+ @field_name = field_name
12
+ @label = label
13
+ @value = value
14
+ @data = data
15
+ @position = position
16
+ super()
17
+ end
18
+
19
+ attr_reader :form, :field_name, :label, :value, :data
20
+
21
+ def label_classes
22
+ merge_classes('btn', @position == :left ? 'rounded-start-pill' : 'rounded-end-pill')
23
+ end
24
+ end
25
+ end
26
+ end