primer_view_components 0.0.17 → 0.0.22

Sign up to get free protection for your applications and to get access to all the features.
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