primer_view_components 0.0.17 → 0.0.22

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -0
  3. data/app/assets/javascripts/primer_view_components.js +2 -0
  4. data/app/assets/javascripts/primer_view_components.js.map +1 -0
  5. data/app/components/primer/avatar_component.rb +27 -9
  6. data/app/components/primer/avatar_stack_component.html.erb +10 -0
  7. data/app/components/primer/avatar_stack_component.rb +81 -0
  8. data/app/components/primer/base_component.rb +8 -5
  9. data/app/components/primer/blankslate_component.html.erb +3 -3
  10. data/app/components/primer/blankslate_component.rb +18 -25
  11. data/app/components/primer/border_box_component.html.erb +4 -18
  12. data/app/components/primer/border_box_component.rb +75 -68
  13. data/app/components/primer/box_component.rb +10 -0
  14. data/app/components/primer/breadcrumb_component.rb +3 -2
  15. data/app/components/primer/button_component.rb +3 -3
  16. data/app/components/primer/button_group_component.html.erb +5 -0
  17. data/app/components/primer/button_group_component.rb +37 -0
  18. data/app/components/primer/button_marketing_component.rb +73 -0
  19. data/app/components/primer/component.rb +16 -0
  20. data/app/components/primer/counter_component.rb +16 -9
  21. data/app/components/primer/details_component.html.erb +2 -6
  22. data/app/components/primer/details_component.rb +28 -37
  23. data/app/components/primer/dropdown/menu_component.html.erb +12 -0
  24. data/app/components/primer/dropdown/menu_component.rb +48 -0
  25. data/app/components/primer/dropdown_component.html.erb +9 -0
  26. data/app/components/primer/dropdown_component.rb +75 -0
  27. data/app/components/primer/dropdown_menu_component.rb +35 -3
  28. data/app/components/primer/flash_component.html.erb +4 -7
  29. data/app/components/primer/flash_component.rb +18 -17
  30. data/app/components/primer/flex_component.rb +47 -9
  31. data/app/components/primer/flex_item_component.rb +16 -1
  32. data/app/components/primer/heading_component.rb +9 -0
  33. data/app/components/primer/label_component.rb +6 -6
  34. data/app/components/primer/layout_component.rb +2 -2
  35. data/app/components/primer/link_component.rb +6 -2
  36. data/app/components/primer/markdown_component.rb +293 -0
  37. data/app/components/primer/menu_component.html.erb +6 -0
  38. data/app/components/primer/menu_component.rb +71 -0
  39. data/app/components/primer/octicon_component.rb +13 -6
  40. data/app/components/primer/popover_component.rb +5 -3
  41. data/app/components/primer/primer.js +1 -0
  42. data/app/components/primer/primer.ts +1 -0
  43. data/app/components/primer/progress_bar_component.rb +6 -6
  44. data/app/components/primer/spinner_component.rb +8 -5
  45. data/app/components/primer/state_component.rb +23 -12
  46. data/app/components/primer/subhead_component.rb +6 -3
  47. data/app/components/primer/tab_container_component.js +1 -0
  48. data/app/components/primer/tab_container_component.rb +41 -0
  49. data/app/components/primer/tab_container_component.ts +1 -0
  50. data/app/components/primer/tab_nav_component.html.erb +17 -0
  51. data/app/components/primer/tab_nav_component.rb +108 -0
  52. data/app/components/primer/text_component.rb +1 -1
  53. data/app/components/primer/timeline_item_component.html.erb +4 -16
  54. data/app/components/primer/timeline_item_component.rb +41 -49
  55. data/app/components/primer/tooltip_component.rb +88 -0
  56. data/app/components/primer/truncate_component.rb +41 -0
  57. data/app/components/primer/underline_nav_component.rb +26 -1
  58. data/{lib → app/lib}/primer/class_name_helper.rb +1 -0
  59. data/app/lib/primer/classify.rb +280 -0
  60. data/app/lib/primer/classify/cache.rb +125 -0
  61. data/{lib → app/lib}/primer/fetch_or_fallback_helper.rb +1 -0
  62. data/{lib → app/lib}/primer/join_style_arguments_helper.rb +1 -0
  63. data/app/lib/primer/view_helper.rb +22 -0
  64. data/app/lib/primer/view_helper/dsl.rb +34 -0
  65. data/lib/primer/view_components.rb +32 -0
  66. data/lib/primer/view_components/engine.rb +11 -2
  67. data/lib/primer/view_components/version.rb +5 -1
  68. data/lib/yard/renders_many_handler.rb +19 -0
  69. data/lib/yard/renders_one_handler.rb +19 -0
  70. data/static/statuses.json +1 -0
  71. metadata +94 -24
  72. data/app/components/primer/view_components.rb +0 -52
  73. data/lib/primer/classify.rb +0 -250
@@ -3,7 +3,7 @@
3
3
  module Primer
4
4
  # The Text component is a wrapper component that will apply typography styles to the text inside.
5
5
  class TextComponent < Primer::Component
6
- # @example 70|Default
6
+ # @example Default
7
7
  # <%= render(Primer::TextComponent.new(tag: :p, font_weight: :bold)) { "Bold Text" } %>
8
8
  # <%= render(Primer::TextComponent.new(tag: :p, color: :red_5)) { "Red Text" } %>
9
9
  #
@@ -1,17 +1,5 @@
1
- <%= render Primer::BaseComponent.new(**system_arguments) do %>
2
- <% if avatar %>
3
- <%= render Primer::AvatarComponent.new(alt: avatar.alt, src: avatar.src, size: avatar.size, square: avatar.square, **avatar.system_arguments) %>
4
- <% end %>
5
-
6
- <% if badge %>
7
- <%= render Primer::BaseComponent.new(**badge.system_arguments) do %>
8
- <%= octicon badge.icon %>
9
- <% end %>
10
- <% end %>
11
-
12
- <% if body %>
13
- <%= render Primer::BaseComponent.new(**body.system_arguments) do %>
14
- <%= body.content %>
15
- <% end %>
16
- <% end %>
1
+ <%= render Primer::BaseComponent.new(**@system_arguments) do %>
2
+ <%= avatar %>
3
+ <%= badge %>
4
+ <%= body %>
17
5
  <% end %>
@@ -3,20 +3,45 @@
3
3
  module Primer
4
4
  # Use `TimelineItem` to display items on a vertical timeline, connected by badge elements.
5
5
  class TimelineItemComponent < Primer::Component
6
- include ViewComponent::Slotable
6
+ include ViewComponent::SlotableV2
7
7
 
8
- with_slot :avatar, class_name: "Avatar"
9
- with_slot :badge, class_name: "Badge"
10
- with_slot :body, class_name: "Body"
8
+ # Avatar to be rendered to the left of the Badge.
9
+ #
10
+ # @param kwargs [Hash] The same arguments as <%= link_to_component(Primer::AvatarComponent) %>.
11
+ renders_one :avatar, lambda { |src:, size: 40, square: true, **system_arguments|
12
+ system_arguments[:classes] = class_names(
13
+ "TimelineItem-avatar",
14
+ system_arguments[:classes]
15
+ )
16
+
17
+ Primer::AvatarComponent.new(src: src, size: size, square: square, **system_arguments)
18
+ }
11
19
 
12
- attr_reader :system_arguments
20
+ # Badge that will be connected to other TimelineItems.
21
+ #
22
+ # @param icon [String] Name of [Octicon](https://primer.style/octicons/) to use.
23
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
24
+ renders_one :badge, "BadgeComponent"
13
25
 
14
- # @example 75|Default
26
+ # Body to be rendered to the left of the Badge.
27
+ #
28
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
29
+ renders_one :body, lambda { |**system_arguments|
30
+ system_arguments[:tag] = :div
31
+ system_arguments[:classes] = class_names(
32
+ "TimelineItem-body",
33
+ system_arguments[:classes]
34
+ )
35
+
36
+ Primer::BaseComponent.new(**system_arguments)
37
+ }
38
+
39
+ # @example Default
15
40
  # <div style="padding-left: 60px">
16
41
  # <%= render(Primer::TimelineItemComponent.new) do |component| %>
17
- # <% component.slot(:avatar, src: "https://github.com/github.png", alt: "github") %>
18
- # <% component.slot(:badge, bg: :green, color: :white, icon: :check) %>
19
- # <% component.slot(:body) { "Success!" } %>
42
+ # <% component.avatar(src: "https://github.com/github.png", alt: "github") %>
43
+ # <% component.badge(bg: :green, color: :white, icon: :check) %>
44
+ # <% component.body { "Success!" } %>
20
45
  # <% end %>
21
46
  # </div>
22
47
  #
@@ -36,34 +61,9 @@ module Primer
36
61
  avatar.present? || badge.present? || body.present?
37
62
  end
38
63
 
39
- class Avatar < Primer::Slot
40
- attr_reader :system_arguments, :alt, :src, :size, :square
41
-
42
- # @param alt [String] Alt text for avatar image.
43
- # @param src [String] Src attribute for avatar image.
44
- # @param size [Integer] Image size.
45
- # @param square [Boolean] Whether to round the edges of the image.
46
- # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
47
- def initialize(alt: nil, src: nil, size: 40, square: true, **system_arguments)
48
- @alt = alt
49
- @src = src
50
- @size = size
51
- @square = square
52
-
53
- @system_arguments = system_arguments
54
- @system_arguments[:tag] = :div
55
- @system_arguments[:classes] = class_names(
56
- "TimelineItem-avatar",
57
- system_arguments[:classes]
58
- )
59
- end
60
- end
61
-
62
- class Badge < Primer::Slot
63
- attr_reader :system_arguments, :icon
64
-
65
- # @param icon [String] Name of [Octicon](https://primer.style/octicons/) to use.
66
- # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
64
+ # This component is part of `Primer::TimelineItemComponent` and should not be
65
+ # used as a standalone component.
66
+ class BadgeComponent < Primer::Component
67
67
  def initialize(icon: nil, **system_arguments)
68
68
  @icon = icon
69
69
 
@@ -74,19 +74,11 @@ module Primer
74
74
  system_arguments[:classes]
75
75
  )
76
76
  end
77
- end
78
-
79
- class Body < Primer::Slot
80
- attr_reader :system_arguments
81
77
 
82
- # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
83
- def initialize(**system_arguments)
84
- @system_arguments = system_arguments
85
- @system_arguments[:tag] = :div
86
- @system_arguments[:classes] = class_names(
87
- "TimelineItem-body",
88
- system_arguments[:classes]
89
- )
78
+ def call
79
+ render(Primer::BaseComponent.new(**@system_arguments)) do
80
+ render(Primer::OcticonComponent.new(icon: @icon))
81
+ end
90
82
  end
91
83
  end
92
84
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ # The Tooltip component is a wrapper component that will apply a tooltip to the provided content.
5
+ class TooltipComponent < Primer::Component
6
+ DIRECTION_DEFAULT = :n
7
+ ALIGN_DEFAULT = :default
8
+ MULTILINE_DEFAULT = false
9
+ DELAY_DEFAULT = false
10
+
11
+ ALIGN_MAPPING = {
12
+ ALIGN_DEFAULT => "",
13
+ :left_1 => "tooltipped-align-left-1",
14
+ :right_1 => "tooltipped-align-right-1",
15
+ :left_2 => "tooltipped-align-left-2",
16
+ :right_2 => "tooltipped-align-right-2"
17
+ }.freeze
18
+
19
+ DIRECTION_OPTIONS = [DIRECTION_DEFAULT] + %i[
20
+ nw
21
+ ne
22
+ w
23
+ e
24
+ sw
25
+ s
26
+ se
27
+ ]
28
+
29
+ # @example Default
30
+ # <div class="pt-5">
31
+ # <%= render(Primer::TooltipComponent.new(label: "Even bolder")) { "Default Bold Text" } %>
32
+ # </div>
33
+ #
34
+ # @example Wrapping another component
35
+ # <div class="pt-5">
36
+ # <%= render(Primer::TooltipComponent.new(label: "Even bolder")) do %>
37
+ # <%= render(Primer::ButtonComponent.new) { "Bold Button" } %>
38
+ # <% end %>
39
+ # </div>
40
+ #
41
+ # @example With a direction
42
+ # <div class="pt-5">
43
+ # <%= render(Primer::TooltipComponent.new(label: "Even bolder", direction: :s)) { "Bold Text With a Direction" } %>
44
+ # </div>
45
+ #
46
+ # @example With an alignment
47
+ # <div class="pt-5">
48
+ # <%= render(Primer::TooltipComponent.new(label: "Even bolder", direction: :s, alignment: :right_1)) { "Bold Text With an Alignment" } %>
49
+ # </div>
50
+ #
51
+ # @example Without a delay
52
+ # <div class="pt-5">
53
+ # <%= render(Primer::TooltipComponent.new(label: "Even bolder", direction: :s, no_delay: true)) { "Bold Text without a delay" } %>
54
+ # </div>
55
+ #
56
+ # @param label [String] the text to appear in the tooltip
57
+ # @param direction [String] Direction of the tooltip. <%= one_of(Primer::TooltipComponent::DIRECTION_OPTIONS) %>
58
+ # @param align [String] Align tooltips to the left or right of an element, combined with a `direction` to specify north or south. <%= one_of(Primer::TooltipComponent::ALIGN_MAPPING.keys) %>
59
+ # @param multiline [Boolean] Use this when you have long content
60
+ # @param no_delay [Boolean] By default the tooltips have a slight delay before appearing. Set true to override this
61
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
62
+ def initialize(
63
+ label:,
64
+ direction: DIRECTION_DEFAULT,
65
+ align: ALIGN_DEFAULT,
66
+ multiline: MULTILINE_DEFAULT,
67
+ no_delay: DELAY_DEFAULT,
68
+ **system_arguments
69
+ )
70
+ @system_arguments = system_arguments
71
+ @system_arguments[:tag] ||= :span
72
+ @system_arguments[:aria] = { label: label }
73
+
74
+ @system_arguments[:classes] = class_names(
75
+ @system_arguments[:classes],
76
+ "tooltipped",
77
+ "tooltipped-#{fetch_or_fallback(DIRECTION_OPTIONS, direction, DIRECTION_DEFAULT)}",
78
+ ALIGN_MAPPING[fetch_or_fallback(ALIGN_MAPPING.keys, align, ALIGN_DEFAULT)],
79
+ "tooltipped-no-delay" => fetch_or_fallback_boolean(no_delay, DELAY_DEFAULT),
80
+ "tooltipped-multiline" => fetch_or_fallback_boolean(multiline, MULTILINE_DEFAULT)
81
+ )
82
+ end
83
+
84
+ def call
85
+ render(Primer::BaseComponent.new(**@system_arguments)) { content }
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ # Use TruncateComponent to shorten overflowing text with an ellipsis.
5
+ class TruncateComponent < Primer::Component
6
+ # @example Default
7
+ # <div class="col-2">
8
+ # <%= render(Primer::TruncateComponent.new(tag: :p)) { "branch-name-that-is-really-long" } %>
9
+ # </div>
10
+ #
11
+ # @example Inline
12
+ # <%= render(Primer::TruncateComponent.new(tag: :span, inline: true)) { "branch-name-that-is-really-long" } %>
13
+ #
14
+ # @example Expandable
15
+ # <%= render(Primer::TruncateComponent.new(tag: :span, inline: true, expandable: true)) { "branch-name-that-is-really-long" } %>
16
+ #
17
+ # @example Custom size
18
+ # <%= render(Primer::TruncateComponent.new(tag: :span, inline: true, expandable: true, max_width: 100)) { "branch-name-that-is-really-long" } %>
19
+ #
20
+ # @param inline [Boolean] Whether the element is inline (or inline-block).
21
+ # @param expandable [Boolean] Whether the entire string should be revealed on hover. Can only be used in conjunction with `inline`.
22
+ # @param max_width [Integer] Sets the max-width of the text.
23
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
24
+ def initialize(inline: false, expandable: false, max_width: nil, **system_arguments)
25
+ @system_arguments = system_arguments
26
+ @system_arguments[:tag] ||= :div
27
+ @system_arguments[:classes] = class_names(
28
+ @system_arguments[:classes],
29
+ "css-truncate",
30
+ "css-truncate-overflow" => !inline,
31
+ "css-truncate-target" => inline,
32
+ "expandable" => inline && expandable
33
+ )
34
+ @system_arguments[:style] = join_style_arguments(@system_arguments[:style], "max-width: #{max_width}px;") unless max_width.nil?
35
+ end
36
+
37
+ def call
38
+ render(Primer::BaseComponent.new(**@system_arguments)) { content }
39
+ end
40
+ end
41
+ end
@@ -1,12 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Primer
4
+ # Use the UnderlineNav component to style navigation with a minimal
5
+ # underlined selected state, typically used for navigation placed at the top
6
+ # of the page.
4
7
  class UnderlineNavComponent < Primer::Component
5
8
  ALIGN_DEFAULT = :left
6
- ALIGN_OPTIONS = [ALIGN_DEFAULT, :right]
9
+ ALIGN_OPTIONS = [ALIGN_DEFAULT, :right].freeze
7
10
 
8
11
  with_content_areas :body, :actions
9
12
 
13
+ # @example Default
14
+ # <%= render(Primer::UnderlineNavComponent.new) do |component| %>
15
+ # <% component.with(:body) do %>
16
+ # <%= render(Primer::LinkComponent.new(href: "#url")) { "Item 1" } %>
17
+ # <% end %>
18
+ # <% component.with(:actions) do %>
19
+ # <%= render(Primer::ButtonComponent.new) { "Button!" } %>
20
+ # <% end %>
21
+ # <% end %>
22
+ #
23
+ # @example Align right
24
+ # <%= render(Primer::UnderlineNavComponent.new(align: :right)) do |component| %>
25
+ # <% component.with(:body) do %>
26
+ # <%= render(Primer::LinkComponent.new(href: "#url")) { "Item 1" } %>
27
+ # <% end %>
28
+ # <% component.with(:actions) do %>
29
+ # <%= render(Primer::ButtonComponent.new) { "Button!" } %>
30
+ # <% end %>
31
+ # <% end %>
32
+ #
33
+ # @param align [Symbol] <%= one_of(Primer::UnderlineNavComponent::ALIGN_OPTIONS) %> - Defaults to <%= Primer::UnderlineNavComponent::ALIGN_DEFAULT %>
34
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
10
35
  def initialize(align: ALIGN_DEFAULT, **system_arguments)
11
36
  @align = fetch_or_fallback(ALIGN_OPTIONS, align, ALIGN_DEFAULT)
12
37
 
@@ -4,6 +4,7 @@
4
4
  #
5
5
  # Helps build a list of conditional class names
6
6
  module Primer
7
+ # :nodoc:
7
8
  module ClassNameHelper
8
9
  def class_names(*args)
9
10
  classes = []
@@ -0,0 +1,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ # :nodoc:
5
+ class Classify
6
+ MARGIN_DIRECTION_KEYS = %i[mt ml mb mr].freeze
7
+ SPACING_KEYS = (%i[m my mx p py px pt pl pb pr] + MARGIN_DIRECTION_KEYS).freeze
8
+ DIRECTION_KEY = :direction
9
+ JUSTIFY_CONTENT_KEY = :justify_content
10
+ ALIGN_ITEMS_KEY = :align_items
11
+ DISPLAY_KEY = :display
12
+ RESPONSIVE_KEYS = ([DISPLAY_KEY, DIRECTION_KEY, JUSTIFY_CONTENT_KEY, ALIGN_ITEMS_KEY, :col, :float] + SPACING_KEYS).freeze
13
+ BREAKPOINTS = ["", "-sm", "-md", "-lg", "-xl"].freeze
14
+
15
+ # Keys where we can simply translate { key: value } into ".key-value"
16
+ CONCAT_KEYS = SPACING_KEYS + %i[hide position v float col text box_shadow].freeze
17
+
18
+ INVALID_CLASS_NAME_PREFIXES =
19
+ (["bg-", "color-", "text-", "d-", "v-align-", "wb-", "text-", "box-shadow-"] + CONCAT_KEYS.map { |k| "#{k}-" }).freeze
20
+ FUNCTIONAL_COLOR_REGEX = /(primary|secondary|tertiary|link|success|warning|danger|info)/.freeze
21
+
22
+ COLOR_KEY = :color
23
+ BG_KEY = :bg
24
+ VERTICAL_ALIGN_KEY = :vertical_align
25
+ WORD_BREAK_KEY = :word_break
26
+ TEXT_KEYS = %i[text_align font_weight].freeze
27
+ FLEX_KEY = :flex
28
+ FLEX_GROW_KEY = :flex_grow
29
+ FLEX_SHRINK_KEY = :flex_shrink
30
+ ALIGN_SELF_KEY = :align_self
31
+ WIDTH_KEY = :width
32
+ HEIGHT_KEY = :height
33
+ BOX_SHADOW_KEY = :box_shadow
34
+ VISIBILITY_KEY = :visibility
35
+ ANIMATION_KEY = :animation
36
+
37
+ BOOLEAN_MAPPINGS = {
38
+ underline: {
39
+ mappings: [
40
+ {
41
+ value: true,
42
+ css_class: "text-underline"
43
+ },
44
+ {
45
+ value: false,
46
+ css_class: "no-underline"
47
+ }
48
+ ]
49
+ },
50
+ top: {
51
+ mappings: [
52
+ {
53
+ value: false,
54
+ css_class: "top-0"
55
+ }
56
+ ]
57
+ },
58
+ bottom: {
59
+ mappings: [
60
+ {
61
+ value: false,
62
+ css_class: "bottom-0"
63
+ }
64
+ ]
65
+ },
66
+ left: {
67
+ mappings: [
68
+ {
69
+ value: false,
70
+ css_class: "left-0"
71
+ }
72
+ ]
73
+ },
74
+ right: {
75
+ mappings: [
76
+ {
77
+ value: false,
78
+ css_class: "right-0"
79
+ }
80
+ ]
81
+ }
82
+ }.freeze
83
+ BORDER_KEYS = %i[border border_color].freeze
84
+ BORDER_MARGIN_KEYS = %i[border_top border_bottom border_left border_right].freeze
85
+ BORDER_RADIUS_KEY = :border_radius
86
+ TYPOGRAPHY_KEYS = [:font_size].freeze
87
+ VALID_KEYS = (
88
+ CONCAT_KEYS +
89
+ BOOLEAN_MAPPINGS.keys +
90
+ BORDER_KEYS +
91
+ BORDER_MARGIN_KEYS +
92
+ TYPOGRAPHY_KEYS +
93
+ TEXT_KEYS +
94
+ [
95
+ BORDER_RADIUS_KEY,
96
+ COLOR_KEY,
97
+ BG_KEY,
98
+ DISPLAY_KEY,
99
+ VERTICAL_ALIGN_KEY,
100
+ WORD_BREAK_KEY,
101
+ DIRECTION_KEY,
102
+ JUSTIFY_CONTENT_KEY,
103
+ ALIGN_ITEMS_KEY,
104
+ FLEX_KEY,
105
+ FLEX_GROW_KEY,
106
+ FLEX_SHRINK_KEY,
107
+ ALIGN_SELF_KEY,
108
+ WIDTH_KEY,
109
+ HEIGHT_KEY,
110
+ BOX_SHADOW_KEY,
111
+ VISIBILITY_KEY,
112
+ ANIMATION_KEY
113
+ ]
114
+ ).freeze
115
+
116
+ class << self
117
+ def call(classes: "", style: nil, **args)
118
+ extracted_results = extract_hash(args)
119
+
120
+ extracted_results[:class] = [
121
+ validated_class_names(classes),
122
+ extracted_results.delete(:classes)
123
+ ].compact.join(" ").presence
124
+
125
+ extracted_results[:style] = [
126
+ extracted_results.delete(:styles),
127
+ style
128
+ ].compact.join("").presence
129
+
130
+ extracted_results
131
+ end
132
+
133
+ private
134
+
135
+ def validated_class_names(classes)
136
+ return if classes.blank?
137
+
138
+ if ENV["RAILS_ENV"] == "development"
139
+ invalid_class_names =
140
+ classes.split(" ").each_with_object([]) do |class_name, memo|
141
+ memo << class_name if INVALID_CLASS_NAME_PREFIXES.any? { |prefix| class_name.start_with?(prefix) }
142
+ end
143
+
144
+ raise ArgumentError, "Use System Arguments (https://primer.style/view-components/system-arguments) instead of Primer CSS class #{'name'.pluralize(invalid_class_names.length)} #{invalid_class_names.to_sentence}. This warning will not be raised in production." if invalid_class_names.any?
145
+ end
146
+
147
+ classes
148
+ end
149
+
150
+ # NOTE: This is a fairly naive implementation that we're building as we go.
151
+ # Feel free to refactor as this is thoroughly tested.
152
+ #
153
+ # Utility for mapping component configuration into Primer CSS class names
154
+ #
155
+ # styles_hash - A hash with utility keys that mimic the interface used by https://github.com/primer/components
156
+ #
157
+ # Returns a string of Primer CSS class names to be added to an HTML class attribute
158
+ #
159
+ # Example usage:
160
+ # extract_hash({ mt: 4, py: 2 }) => "mt-4 py-2"
161
+ def extract_hash(styles_hash)
162
+ memo = { classes: [], styles: +"" }
163
+ styles_hash.each do |key, value|
164
+ next unless VALID_KEYS.include?(key)
165
+
166
+ if value.is_a?(Array)
167
+ raise ArgumentError, "#{key} does not support responsive values" unless RESPONSIVE_KEYS.include?(key)
168
+
169
+ value.each_with_index do |val, index|
170
+ Primer::Classify::Cache.read(memo, key, val, BREAKPOINTS[index]) || extract_value(memo, key, val, BREAKPOINTS[index])
171
+ end
172
+ else
173
+ Primer::Classify::Cache.read(memo, key, value, BREAKPOINTS[0]) || extract_value(memo, key, value, BREAKPOINTS[0])
174
+ end
175
+ end
176
+
177
+ memo[:classes] = memo[:classes].join(" ")
178
+
179
+ memo
180
+ end
181
+
182
+ def extract_value(memo, key, val, breakpoint)
183
+ return if val.nil? || val == ""
184
+
185
+ if SPACING_KEYS.include?(key)
186
+ if MARGIN_DIRECTION_KEYS.include?(key)
187
+ raise ArgumentError, "value of #{key} must be between -6 and 6" if val < -6 || val > 6
188
+ elsif !((key == :mx || key == :my) && val == :auto)
189
+ raise ArgumentError, "value of #{key} must be between 0 and 6" if val.negative? || val > 6
190
+ end
191
+ end
192
+
193
+ if BOOLEAN_MAPPINGS.key?(key)
194
+ BOOLEAN_MAPPINGS[key][:mappings].map { |m| m[:css_class] if m[:value] == val }.compact.each do |css_class|
195
+ memo[:classes] << css_class
196
+ end
197
+ elsif key == BG_KEY
198
+ if val.to_s.start_with?("#")
199
+ memo[:styles] << "background-color: #{val};"
200
+ else
201
+ memo[:classes] << "bg-#{val.to_s.dasherize}"
202
+ end
203
+ elsif key == COLOR_KEY
204
+ char_code = val[-1].ord
205
+ # Does this string end in a character that is NOT a number?
206
+ memo[:classes] <<
207
+ if (char_code >= 48 && char_code <= 57) || # 48 is the charcode for 0; 57 is the charcode for 9
208
+ FUNCTIONAL_COLOR_REGEX.match?(val)
209
+ "color-#{val.to_s.dasherize}"
210
+ else
211
+ "text-#{val.to_s.dasherize}"
212
+ end
213
+ elsif key == DISPLAY_KEY
214
+ memo[:classes] << "d#{breakpoint}-#{val.to_s.dasherize}"
215
+ elsif key == VERTICAL_ALIGN_KEY
216
+ memo[:classes] << "v-align-#{val.to_s.dasherize}"
217
+ elsif key == WORD_BREAK_KEY
218
+ memo[:classes] << "wb-#{val.to_s.dasherize}"
219
+ elsif BORDER_KEYS.include?(key)
220
+ border_value = if val == true
221
+ "border"
222
+ else
223
+ "border-#{val.to_s.dasherize}"
224
+ end
225
+
226
+ memo[:classes] << border_value
227
+ elsif BORDER_MARGIN_KEYS.include?(key)
228
+ memo[:classes] << "#{key.to_s.dasherize}-#{val}"
229
+ elsif key == BORDER_RADIUS_KEY
230
+ memo[:classes] << "rounded-#{val}"
231
+ elsif key == DIRECTION_KEY
232
+ memo[:classes] << "flex#{breakpoint}-#{val.to_s.dasherize}"
233
+ elsif key == JUSTIFY_CONTENT_KEY
234
+ formatted_value = val.to_s.gsub(/(flex\_|space\_)/, "")
235
+ memo[:classes] << "flex#{breakpoint}-justify-#{formatted_value}"
236
+ elsif key == ALIGN_ITEMS_KEY
237
+ memo[:classes] << "flex#{breakpoint}-items-#{val.to_s.gsub('flex_', '')}"
238
+ elsif key == FLEX_KEY
239
+ memo[:classes] << "flex-#{val}"
240
+ elsif key == FLEX_GROW_KEY
241
+ memo[:classes] << "flex-grow-#{val}"
242
+ elsif key == FLEX_SHRINK_KEY
243
+ memo[:classes] << "flex-shrink-#{val}"
244
+ elsif key == ALIGN_SELF_KEY
245
+ memo[:classes] << "flex-self-#{val}"
246
+ elsif key == WIDTH_KEY || key == HEIGHT_KEY
247
+ if val == :fit || val == :fill
248
+ memo[:classes] << "#{key}-#{val}"
249
+ else
250
+ memo[key] = val
251
+ end
252
+ elsif TEXT_KEYS.include?(key)
253
+ memo[:classes] << "text-#{val.to_s.dasherize}"
254
+ elsif TYPOGRAPHY_KEYS.include?(key)
255
+ memo[:classes] << "f#{val.to_s.dasherize}"
256
+ elsif MARGIN_DIRECTION_KEYS.include?(key) && val.negative?
257
+ memo[:classes] << "#{key.to_s.dasherize}#{breakpoint}-n#{val.abs}"
258
+ elsif key == BOX_SHADOW_KEY
259
+ memo[:classes] << if val == true
260
+ "box-shadow"
261
+ else
262
+ "box-shadow-#{val.to_s.dasherize}"
263
+ end
264
+ elsif key == VISIBILITY_KEY
265
+ memo[:classes] << "v-#{val.to_s.dasherize}"
266
+ elsif key == ANIMATION_KEY
267
+ memo[:classes] << if val == :grow
268
+ "hover-grow"
269
+ else
270
+ "anim-#{val.to_s.dasherize}"
271
+ end
272
+ else
273
+ memo[:classes] << "#{key.to_s.dasherize}#{breakpoint}-#{val.to_s.dasherize}"
274
+ end
275
+ end
276
+ end
277
+
278
+ Cache.preload!
279
+ end
280
+ end