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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +102 -0
- data/app/assets/javascripts/primer_view_components.js +2 -0
- data/app/assets/javascripts/primer_view_components.js.map +1 -0
- data/app/components/primer/avatar_component.rb +27 -9
- data/app/components/primer/avatar_stack_component.html.erb +10 -0
- data/app/components/primer/avatar_stack_component.rb +81 -0
- data/app/components/primer/base_component.rb +8 -5
- data/app/components/primer/blankslate_component.html.erb +3 -3
- data/app/components/primer/blankslate_component.rb +18 -25
- data/app/components/primer/border_box_component.html.erb +4 -18
- data/app/components/primer/border_box_component.rb +75 -68
- data/app/components/primer/box_component.rb +10 -0
- data/app/components/primer/breadcrumb_component.rb +3 -2
- data/app/components/primer/button_component.rb +3 -3
- data/app/components/primer/button_group_component.html.erb +5 -0
- data/app/components/primer/button_group_component.rb +37 -0
- data/app/components/primer/button_marketing_component.rb +73 -0
- data/app/components/primer/component.rb +16 -0
- data/app/components/primer/counter_component.rb +16 -9
- data/app/components/primer/details_component.html.erb +2 -6
- data/app/components/primer/details_component.rb +28 -37
- data/app/components/primer/dropdown/menu_component.html.erb +12 -0
- data/app/components/primer/dropdown/menu_component.rb +48 -0
- data/app/components/primer/dropdown_component.html.erb +9 -0
- data/app/components/primer/dropdown_component.rb +75 -0
- data/app/components/primer/dropdown_menu_component.rb +35 -3
- data/app/components/primer/flash_component.html.erb +4 -7
- data/app/components/primer/flash_component.rb +18 -17
- data/app/components/primer/flex_component.rb +47 -9
- data/app/components/primer/flex_item_component.rb +16 -1
- data/app/components/primer/heading_component.rb +9 -0
- data/app/components/primer/label_component.rb +6 -6
- data/app/components/primer/layout_component.rb +2 -2
- data/app/components/primer/link_component.rb +6 -2
- data/app/components/primer/markdown_component.rb +293 -0
- data/app/components/primer/menu_component.html.erb +6 -0
- data/app/components/primer/menu_component.rb +71 -0
- data/app/components/primer/octicon_component.rb +13 -6
- data/app/components/primer/popover_component.rb +5 -3
- data/app/components/primer/primer.js +1 -0
- data/app/components/primer/primer.ts +1 -0
- data/app/components/primer/progress_bar_component.rb +6 -6
- data/app/components/primer/spinner_component.rb +8 -5
- data/app/components/primer/state_component.rb +23 -12
- data/app/components/primer/subhead_component.rb +6 -3
- data/app/components/primer/tab_container_component.js +1 -0
- data/app/components/primer/tab_container_component.rb +41 -0
- data/app/components/primer/tab_container_component.ts +1 -0
- data/app/components/primer/tab_nav_component.html.erb +17 -0
- data/app/components/primer/tab_nav_component.rb +108 -0
- data/app/components/primer/text_component.rb +1 -1
- data/app/components/primer/timeline_item_component.html.erb +4 -16
- data/app/components/primer/timeline_item_component.rb +41 -49
- data/app/components/primer/tooltip_component.rb +88 -0
- data/app/components/primer/truncate_component.rb +41 -0
- data/app/components/primer/underline_nav_component.rb +26 -1
- data/{lib → app/lib}/primer/class_name_helper.rb +1 -0
- data/app/lib/primer/classify.rb +280 -0
- data/app/lib/primer/classify/cache.rb +125 -0
- data/{lib → app/lib}/primer/fetch_or_fallback_helper.rb +1 -0
- data/{lib → app/lib}/primer/join_style_arguments_helper.rb +1 -0
- data/app/lib/primer/view_helper.rb +22 -0
- data/app/lib/primer/view_helper/dsl.rb +34 -0
- data/lib/primer/view_components.rb +32 -0
- data/lib/primer/view_components/engine.rb +11 -2
- data/lib/primer/view_components/version.rb +5 -1
- data/lib/yard/renders_many_handler.rb +19 -0
- data/lib/yard/renders_one_handler.rb +19 -0
- data/static/statuses.json +1 -0
- metadata +94 -24
- data/app/components/primer/view_components.rb +0 -52
- 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
|
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(
|
2
|
-
|
3
|
-
|
4
|
-
|
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::
|
6
|
+
include ViewComponent::SlotableV2
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
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
|
-
#
|
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.
|
18
|
-
# <% component.
|
19
|
-
# <% component.
|
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
|
-
|
40
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
|
@@ -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
|