primer_view_components 0.0.47 → 0.0.51
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 +113 -0
- data/app/components/primer/base_component.rb +2 -2
- data/app/components/primer/beta/auto_complete.rb +2 -2
- data/app/components/primer/beta/avatar.rb +1 -1
- data/app/components/primer/{avatar_stack_component.html.erb → beta/avatar_stack.html.erb} +0 -0
- data/app/components/primer/beta/avatar_stack.rb +92 -0
- data/app/components/primer/clipboard_copy.html.erb +2 -2
- data/app/components/primer/component.rb +5 -1
- data/app/components/primer/image_crop.html.erb +4 -4
- data/app/components/primer/label_component.rb +13 -12
- data/app/components/primer/navigation/tab_component.rb +30 -2
- data/app/components/primer/tab_nav_component.rb +4 -3
- data/app/components/primer/truncate.rb +1 -1
- data/app/components/primer/underline_nav_component.rb +3 -2
- data/app/lib/primer/octicon/cache.rb +1 -1
- data/lib/primer/classify.rb +0 -10
- data/lib/primer/classify/cache.rb +0 -5
- data/lib/primer/classify/utilities.rb +35 -12
- data/lib/primer/classify/utilities.yml +16 -0
- data/lib/primer/view_components.rb +34 -6
- data/lib/primer/view_components/constants.rb +55 -0
- data/lib/primer/view_components/linters/argument_mappers/base.rb +74 -0
- data/lib/primer/view_components/linters/argument_mappers/button.rb +32 -44
- data/lib/primer/view_components/linters/argument_mappers/clipboard_copy.rb +20 -0
- data/lib/primer/view_components/linters/argument_mappers/helpers/erb_block.rb +24 -0
- data/lib/primer/view_components/linters/argument_mappers/label.rb +50 -0
- data/lib/primer/view_components/linters/argument_mappers/system_arguments.rb +4 -1
- data/lib/primer/view_components/linters/autocorrectable.rb +30 -0
- data/lib/primer/view_components/linters/button_component_migration_counter.rb +9 -23
- data/lib/primer/view_components/linters/clipboard_copy_component_migration_counter.rb +21 -0
- data/lib/primer/view_components/linters/close_button_component_migration_counter.rb +16 -0
- data/lib/primer/view_components/linters/helpers.rb +42 -41
- data/lib/primer/view_components/linters/label_component_migration_counter.rb +25 -0
- data/lib/primer/view_components/version.rb +1 -1
- data/lib/rubocop/config/default.yml +12 -0
- data/lib/rubocop/cop/primer.rb +4 -0
- data/lib/rubocop/cop/primer/no_tag_memoize.rb +42 -0
- data/lib/rubocop/cop/primer/system_argument_instead_of_class.rb +85 -0
- data/lib/tasks/constants.rake +12 -0
- data/lib/tasks/docs.rake +24 -23
- data/lib/tasks/utilities.rake +4 -10
- data/lib/yard/docs_helper.rb +12 -3
- data/static/arguments.yml +973 -0
- data/static/assets/view-components.svg +18 -0
- data/static/classes.yml +174 -0
- data/static/constants.json +628 -0
- data/static/statuses.json +1 -1
- metadata +26 -8
- data/app/components/primer/avatar_stack_component.rb +0 -90
@@ -19,6 +19,7 @@ module Primer
|
|
19
19
|
# Use the tabs to list navigation items. When `with_panel` is set on the parent, a button is rendered for panel navigation. Otherwise,
|
20
20
|
# an anchor tag is rendered for page navigation. For more information, refer to <%= link_to_component(Primer::Navigation::TabComponent) %>.
|
21
21
|
#
|
22
|
+
# @param panel_id [String] Only applies if `with_panel` is `true`. Unique id of panel.
|
22
23
|
# @param selected [Boolean] Whether the tab is selected.
|
23
24
|
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
24
25
|
renders_many :tabs, lambda { |selected: false, **system_arguments|
|
@@ -104,13 +105,13 @@ module Primer
|
|
104
105
|
#
|
105
106
|
# @example With panels
|
106
107
|
# <%= render(Primer::UnderlineNavComponent.new(label: "With panels", with_panel: true)) do |component| %>
|
107
|
-
# <% component.tab(selected: true) do |t| %>
|
108
|
+
# <% component.tab(selected: true, id: "tab-1", panel_id: "panel-1") do |t| %>
|
108
109
|
# <% t.text { "Item 1" } %>
|
109
110
|
# <% t.panel do %>
|
110
111
|
# Panel 1
|
111
112
|
# <% end %>
|
112
113
|
# <% end %>
|
113
|
-
# <% component.tab do |t| %>
|
114
|
+
# <% component.tab(id: "tab-2", panel_id: "panel-2") do |t| %>
|
114
115
|
# <% t.text { "Item 2" } %>
|
115
116
|
# <% t.panel do %>
|
116
117
|
# Panel 2
|
@@ -6,7 +6,7 @@ module Primer
|
|
6
6
|
class Cache
|
7
7
|
LOOKUP = {} # rubocop:disable Style/MutableConstant
|
8
8
|
# Preload the top 20 used icons.
|
9
|
-
PRELOADED_ICONS = [:alert, :check, :"chevron-down", :
|
9
|
+
PRELOADED_ICONS = [:alert, :check, :"chevron-down", :paste, :clock, :"dot-fill", :info, :"kebab-horizontal", :link, :lock, :mail, :pencil, :plus, :question, :repo, :search, :"shield-lock", :star, :trash, :x].freeze
|
10
10
|
|
11
11
|
class << self
|
12
12
|
def get_key(symbol:, size:, width: nil, height: nil)
|
data/lib/primer/classify.rb
CHANGED
@@ -20,8 +20,6 @@ module Primer
|
|
20
20
|
COLOR_KEY = :color
|
21
21
|
BG_KEY = :bg
|
22
22
|
TEXT_KEYS = %i[font_family font_style font_weight text_align text_transform].freeze
|
23
|
-
WIDTH_KEY = :width
|
24
|
-
HEIGHT_KEY = :height
|
25
23
|
BOX_SHADOW_KEY = :box_shadow
|
26
24
|
CONTAINER_KEY = :container
|
27
25
|
|
@@ -94,8 +92,6 @@ module Primer
|
|
94
92
|
BORDER_RADIUS_KEY,
|
95
93
|
COLOR_KEY,
|
96
94
|
BG_KEY,
|
97
|
-
WIDTH_KEY,
|
98
|
-
HEIGHT_KEY,
|
99
95
|
BOX_SHADOW_KEY,
|
100
96
|
CONTAINER_KEY
|
101
97
|
]
|
@@ -202,12 +198,6 @@ module Primer
|
|
202
198
|
memo[:classes] << Primer::Classify::Flex.classes(key, val, breakpoint)
|
203
199
|
elsif Primer::Classify::Grid::KEYS.include?(key)
|
204
200
|
memo[:classes] << Primer::Classify::Grid.classes(key, val, breakpoint)
|
205
|
-
elsif key == WIDTH_KEY || key == HEIGHT_KEY
|
206
|
-
if val == :fit
|
207
|
-
memo[:classes] << "#{key}-#{val}"
|
208
|
-
else
|
209
|
-
memo[key] = val
|
210
|
-
end
|
211
201
|
elsif TEXT_KEYS.include?(key)
|
212
202
|
memo[:classes] << "text-#{val.to_s.dasherize}"
|
213
203
|
elsif TYPOGRAPHY_KEYS.include?(key)
|
@@ -95,11 +95,6 @@ module Primer
|
|
95
95
|
values: Primer::Classify::Flex::ALIGN_SELF_VALUES
|
96
96
|
)
|
97
97
|
|
98
|
-
preload(
|
99
|
-
keys: [Primer::Classify::WIDTH_KEY, Primer::Classify::HEIGHT_KEY],
|
100
|
-
values: [:fit]
|
101
|
-
)
|
102
|
-
|
103
98
|
preload(
|
104
99
|
keys: Primer::Classify::BOX_SHADOW_KEY,
|
105
100
|
values: [true, :small, :medium, :large, :extra_large, :none]
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "yaml"
|
4
|
+
|
3
5
|
# :nodoc:
|
4
6
|
module Primer
|
5
7
|
class Classify
|
@@ -16,6 +18,17 @@ module Primer
|
|
16
18
|
# rubocop:enable Security/YAMLLoad
|
17
19
|
BREAKPOINTS = ["", "-sm", "-md", "-lg", "-xl"].freeze
|
18
20
|
|
21
|
+
# Replacements for some classnames that end up being a different argument key
|
22
|
+
REPLACEMENT_KEYS = {
|
23
|
+
"^anim" => "animation",
|
24
|
+
"^v-align" => "vertical_align",
|
25
|
+
"^d" => "display",
|
26
|
+
"^wb" => "word_break",
|
27
|
+
"^v" => "visibility",
|
28
|
+
"^width" => "w",
|
29
|
+
"^height" => "h"
|
30
|
+
}.freeze
|
31
|
+
|
19
32
|
class << self
|
20
33
|
def classname(key, val, breakpoint = "")
|
21
34
|
if (valid = validate(key, val, breakpoint))
|
@@ -104,23 +117,33 @@ module Primer
|
|
104
117
|
private
|
105
118
|
|
106
119
|
def find_selector(selector)
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
120
|
+
key = infer_selector_key(selector)
|
121
|
+
value_hash = UTILITIES[key]
|
122
|
+
|
123
|
+
return nil if value_hash.blank?
|
124
|
+
|
125
|
+
# Each value hash will also contain an array of classnames for breakpoints
|
126
|
+
# Key argument `0`, classes `[ "mr-0", "mr-sm-0", "mr-md-0", "mr-lg-0", "mr-xl-0" ]`
|
127
|
+
value_hash.each do |key_argument, classnames|
|
128
|
+
# Skip each value hash until we get one with the selector
|
129
|
+
next unless classnames.include?(selector)
|
130
|
+
|
131
|
+
# Return [:mr, 0, 1]
|
132
|
+
# has index of classname, so we can match it up with responsvie array `mr: [nil, 0]`
|
133
|
+
return [key, key_argument, classnames.index(selector)]
|
119
134
|
end
|
120
135
|
|
121
136
|
nil
|
122
137
|
end
|
123
138
|
|
139
|
+
def infer_selector_key(selector)
|
140
|
+
REPLACEMENT_KEYS.each do |k, v|
|
141
|
+
return v.to_sym if selector.match?(Regexp.new(k))
|
142
|
+
end
|
143
|
+
|
144
|
+
selector.split("-").first.to_sym
|
145
|
+
end
|
146
|
+
|
124
147
|
def validate(key, val, breakpoint)
|
125
148
|
unless supported_key?(key)
|
126
149
|
raise ArgumentError, "#{key} is not a valid Primer utility key" unless ENV["RAILS_ENV"] == "production"
|
@@ -85,6 +85,22 @@
|
|
85
85
|
- float-md-none
|
86
86
|
- float-lg-none
|
87
87
|
- float-xl-none
|
88
|
+
:w:
|
89
|
+
:fit:
|
90
|
+
- width-fit
|
91
|
+
:full:
|
92
|
+
- width-full
|
93
|
+
:auto:
|
94
|
+
- width-auto
|
95
|
+
- width-sm-auto
|
96
|
+
- width-md-auto
|
97
|
+
- width-lg-auto
|
98
|
+
- width-xl-auto
|
99
|
+
:h:
|
100
|
+
:fit:
|
101
|
+
- height-fit
|
102
|
+
:full:
|
103
|
+
- height-full
|
88
104
|
:m:
|
89
105
|
0:
|
90
106
|
- m-0
|
@@ -7,22 +7,21 @@ require "primer/view_components/engine"
|
|
7
7
|
module Primer
|
8
8
|
# :nodoc:
|
9
9
|
module ViewComponents
|
10
|
-
|
10
|
+
DEFAULT_STATIC_PATH = File.expand_path("static")
|
11
11
|
DEFAULT_STATUS_FILE_NAME = "statuses.json"
|
12
|
+
DEFAULT_CONSTANTS_FILE_NAME = "constants.json"
|
12
13
|
|
13
14
|
# generate_statuses returns a hash mapping component name to
|
14
15
|
# the component's status sorted alphabetically by the component name.
|
15
16
|
def self.generate_statuses
|
16
|
-
|
17
|
+
Primer::Component.descendants.sort_by(&:name).each_with_object({}) do |component, mem|
|
17
18
|
mem[component.to_s] = component.status.to_s
|
18
19
|
end
|
19
|
-
|
20
|
-
statuses.sort_by { |k, _v| k }.to_h
|
21
20
|
end
|
22
21
|
|
23
22
|
# dump_statuses generates the status hash and then serializes
|
24
23
|
# it as json at the given path
|
25
|
-
def self.dump_statuses(path:
|
24
|
+
def self.dump_statuses(path: DEFAULT_STATIC_PATH)
|
26
25
|
require "json"
|
27
26
|
|
28
27
|
statuses = generate_statuses
|
@@ -35,8 +34,37 @@ module Primer
|
|
35
34
|
|
36
35
|
# read_statuses returns a JSON string matching the output of
|
37
36
|
# generate_statuses
|
38
|
-
def self.read_statuses(path:
|
37
|
+
def self.read_statuses(path: DEFAULT_STATIC_PATH)
|
39
38
|
File.read(File.join(path, DEFAULT_STATUS_FILE_NAME))
|
40
39
|
end
|
40
|
+
|
41
|
+
# generate_constants returns a hash mapping component name to
|
42
|
+
# all of its constants.
|
43
|
+
def self.generate_constants
|
44
|
+
Primer::Component.descendants.sort_by(&:name).each_with_object({}) do |component, mem|
|
45
|
+
mem[component.to_s] = component.constants(false).sort.each_with_object({}) do |constant, h|
|
46
|
+
h[constant] = component.const_get(constant)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# dump_constants generates the constants hash and then serializes
|
52
|
+
# it as json at the given path
|
53
|
+
def self.dump_constants(path: DEFAULT_STATIC_PATH)
|
54
|
+
require "json"
|
55
|
+
|
56
|
+
constants = generate_constants
|
57
|
+
|
58
|
+
File.open(File.join(path, DEFAULT_CONSTANTS_FILE_NAME), "w") do |f|
|
59
|
+
f.write(JSON.pretty_generate(constants))
|
60
|
+
f.write($INPUT_RECORD_SEPARATOR)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# read_constants returns a JSON string matching the output of
|
65
|
+
# generate_constants
|
66
|
+
def self.read_constants(path: DEFAULT_STATIC_PATH)
|
67
|
+
File.read(File.join(path, DEFAULT_CONSTANTS_FILE_NAME))
|
68
|
+
end
|
41
69
|
end
|
42
70
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Primer
|
6
|
+
module ViewComponents
|
7
|
+
# A module for constants that are used in the view components.
|
8
|
+
class Constants
|
9
|
+
CONSTANTS = JSON.parse(
|
10
|
+
File.read(
|
11
|
+
File.join(File.dirname(__FILE__), "../../../static/constants.json")
|
12
|
+
)
|
13
|
+
).freeze
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def get(component:, constant:, invert: true, symbolize: false)
|
17
|
+
values = CONSTANTS.dig(component, constant)
|
18
|
+
|
19
|
+
case values
|
20
|
+
when Hash
|
21
|
+
format_hash(values, invert, symbolize)
|
22
|
+
when Array
|
23
|
+
format_array(values, symbolize)
|
24
|
+
else
|
25
|
+
values
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def format_hash(values, invert, symbolize)
|
32
|
+
val = values.invert if invert
|
33
|
+
# remove defaults
|
34
|
+
val = val.except("", nil)
|
35
|
+
|
36
|
+
return val.transform_values { |v| symbolize_value(v) } if symbolize
|
37
|
+
|
38
|
+
val
|
39
|
+
end
|
40
|
+
|
41
|
+
def format_array(values, symbolize)
|
42
|
+
val = values.select(&:present?)
|
43
|
+
|
44
|
+
return val.map { |v| symbolize_value(v) } if symbolize
|
45
|
+
|
46
|
+
val
|
47
|
+
end
|
48
|
+
|
49
|
+
def symbolize_value(value)
|
50
|
+
":#{value}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "primer/view_components/constants"
|
4
|
+
require "primer/classify/utilities"
|
5
|
+
require_relative "conversion_error"
|
6
|
+
require_relative "system_arguments"
|
7
|
+
require_relative "helpers/erb_block"
|
8
|
+
|
9
|
+
module ERBLint
|
10
|
+
module Linters
|
11
|
+
module ArgumentMappers
|
12
|
+
# Provides the base interface to implement an `ArgumentMapper`.
|
13
|
+
# Override attribute_to_args in a child class to customize its mapping behavior.
|
14
|
+
class Base
|
15
|
+
DEFAULT_TAG = nil
|
16
|
+
ATTRIBUTES = [].freeze
|
17
|
+
|
18
|
+
def initialize(tag)
|
19
|
+
@tag = tag
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
to_args.map { |k, v| "#{k}: #{v}" }.join(", ")
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_args
|
27
|
+
args = {}
|
28
|
+
|
29
|
+
args[:tag] = ":#{@tag.name}" unless self.class::DEFAULT_TAG.nil? || @tag.name == self.class::DEFAULT_TAG
|
30
|
+
|
31
|
+
@tag.attributes.each do |attribute|
|
32
|
+
attr_name = attribute.name
|
33
|
+
|
34
|
+
if self.class::ATTRIBUTES.include?(attr_name)
|
35
|
+
args.merge!(attribute_to_args(attribute))
|
36
|
+
elsif attr_name == "class"
|
37
|
+
args.merge!(map_classes(attribute))
|
38
|
+
else
|
39
|
+
# Assume the attribute is a system argument.
|
40
|
+
args.merge!(SystemArguments.new(attribute).to_args)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
args
|
45
|
+
end
|
46
|
+
|
47
|
+
def attribute_to_args(attribute); end
|
48
|
+
|
49
|
+
def map_classes(classes)
|
50
|
+
system_arguments = system_arguments_to_args(classes.value)
|
51
|
+
args = classes_to_args(system_arguments[:classes])
|
52
|
+
|
53
|
+
args.merge(system_arguments.except(:classes))
|
54
|
+
end
|
55
|
+
|
56
|
+
# Override this with your component's mappings
|
57
|
+
def classes_to_args(classes)
|
58
|
+
raise ConversionError, "Cannot convert classes `#{classes}`" if classes.present?
|
59
|
+
|
60
|
+
{}
|
61
|
+
end
|
62
|
+
|
63
|
+
def system_arguments_to_args(classes)
|
64
|
+
system_arguments = Primer::Classify::Utilities.classes_to_hash(classes)
|
65
|
+
|
66
|
+
# need to transform symbols to strings with leading `:`
|
67
|
+
system_arguments.transform_values do |v|
|
68
|
+
v.is_a?(Symbol) ? ":#{v}" : v
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -1,66 +1,54 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "
|
4
|
-
require_relative "system_arguments"
|
3
|
+
require_relative "base"
|
5
4
|
|
6
5
|
module ERBLint
|
7
6
|
module Linters
|
8
7
|
module ArgumentMappers
|
9
8
|
# Maps classes in a button element to arguments for the Button component.
|
10
|
-
class Button
|
11
|
-
|
12
|
-
"btn-primary" => ":primary",
|
13
|
-
"btn-danger" => ":danger",
|
14
|
-
"btn-outline" => ":outline",
|
15
|
-
"btn-invisible" => ":invisible",
|
16
|
-
"btn-link" => ":link"
|
17
|
-
}.freeze
|
9
|
+
class Button < Base
|
10
|
+
require "pry"
|
18
11
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
12
|
+
SCHEME_MAPPINGS = Primer::ViewComponents::Constants.get(
|
13
|
+
component: "Primer::ButtonComponent",
|
14
|
+
constant: "SCHEME_MAPPINGS",
|
15
|
+
symbolize: true
|
16
|
+
).freeze
|
23
17
|
|
24
|
-
|
18
|
+
VARIANT_MAPPINGS = Primer::ViewComponents::Constants.get(
|
19
|
+
component: "Primer::ButtonComponent",
|
20
|
+
constant: "VARIANT_MAPPINGS",
|
21
|
+
symbolize: true
|
22
|
+
).freeze
|
25
23
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
def to_args
|
35
|
-
args = {}
|
24
|
+
TYPE_OPTIONS = Primer::ViewComponents::Constants.get(
|
25
|
+
component: "Primer::BaseButton",
|
26
|
+
constant: "TYPE_OPTIONS"
|
27
|
+
).freeze
|
28
|
+
DEFAULT_TAG = Primer::ViewComponents::Constants.get(
|
29
|
+
component: "Primer::BaseButton",
|
30
|
+
constant: "DEFAULT_TAG"
|
31
|
+
).freeze
|
36
32
|
|
37
|
-
|
33
|
+
ATTRIBUTES = %w[disabled type].freeze
|
38
34
|
|
39
|
-
|
40
|
-
|
35
|
+
def attribute_to_args(attribute)
|
36
|
+
attr_name = attribute.name
|
41
37
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
# button is the default type, so we don't need to do anything.
|
48
|
-
next if attribute.value == "button"
|
38
|
+
if attr_name == "disabled"
|
39
|
+
{ disabled: true }
|
40
|
+
elsif attr_name == "type"
|
41
|
+
# button is the default type, so we don't need to do anything.
|
42
|
+
return {} if attribute.value == "button"
|
49
43
|
|
50
|
-
|
44
|
+
raise ConversionError, "Button component does not support type \"#{attribute.value}\"" unless TYPE_OPTIONS.include?(attribute.value)
|
51
45
|
|
52
|
-
|
53
|
-
else
|
54
|
-
# Assume the attribute is a system argument.
|
55
|
-
args.merge!(SystemArguments.new(attribute).to_args)
|
56
|
-
end
|
46
|
+
{ type: ":#{attribute.value}" }
|
57
47
|
end
|
58
|
-
|
59
|
-
args
|
60
48
|
end
|
61
49
|
|
62
50
|
def classes_to_args(classes)
|
63
|
-
classes.
|
51
|
+
classes.split(" ").each_with_object({}) do |class_name, acc|
|
64
52
|
next if class_name == "btn"
|
65
53
|
|
66
54
|
if SCHEME_MAPPINGS[class_name] && acc[:scheme].nil?
|