primer_view_components 0.0.48 → 0.0.52
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 +155 -0
- data/app/components/primer/base_component.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/beta/truncate.html.erb +5 -0
- data/app/components/primer/beta/truncate.rb +110 -0
- data/app/components/primer/border_box_component.rb +27 -1
- data/app/components/primer/clipboard_copy.html.erb +2 -2
- data/app/components/primer/clipboard_copy.rb +1 -1
- data/app/components/primer/dropdown.rb +7 -7
- data/app/components/primer/icon_button.rb +1 -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 +16 -2
- data/app/components/primer/progress_bar_component.rb +0 -3
- 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/fetch_or_fallback_helper.rb +2 -0
- data/app/lib/primer/octicon/cache.rb +1 -1
- data/app/lib/primer/tabbed_component_helper.rb +1 -1
- data/app/lib/primer/view_helper.rb +1 -0
- data/lib/primer/classify.rb +4 -16
- data/lib/primer/classify/cache.rb +0 -5
- data/lib/primer/classify/flex.rb +1 -1
- data/lib/primer/classify/functional_colors.rb +1 -1
- data/lib/primer/classify/utilities.rb +51 -13
- data/lib/primer/classify/utilities.yml +16 -0
- data/lib/primer/classify/validation.rb +18 -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 +100 -0
- data/lib/primer/view_components/linters/argument_mappers/button.rb +33 -46
- data/lib/primer/view_components/linters/argument_mappers/clipboard_copy.rb +19 -0
- data/lib/primer/view_components/linters/argument_mappers/helpers/erb_block.rb +67 -0
- data/lib/primer/view_components/linters/argument_mappers/label.rb +49 -0
- data/lib/primer/view_components/linters/argument_mappers/system_arguments.rb +6 -5
- 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 +47 -42
- 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 +5 -0
- data/lib/rubocop/cop/primer.rb +1 -2
- data/lib/rubocop/cop/primer/deprecated_arguments.rb +173 -0
- data/lib/rubocop/cop/primer/no_tag_memoize.rb +1 -0
- data/lib/rubocop/cop/primer/primer_octicon.rb +178 -0
- data/lib/rubocop/cop/primer/system_argument_instead_of_class.rb +12 -16
- data/lib/tasks/constants.rake +12 -0
- data/lib/tasks/coverage.rake +4 -0
- data/lib/tasks/docs.rake +27 -25
- data/lib/tasks/utilities.rake +9 -13
- data/lib/yard/docs_helper.rb +15 -5
- data/static/arguments.yml +980 -0
- data/static/assets/view-components.svg +18 -0
- data/static/classes.yml +182 -0
- data/static/constants.json +640 -0
- data/static/statuses.json +4 -2
- metadata +29 -10
- data/app/components/primer/avatar_stack_component.rb +0 -90
@@ -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
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "utilities"
|
4
|
+
|
5
|
+
module Primer
|
6
|
+
class Classify
|
7
|
+
# :nodoc:
|
8
|
+
class Validation
|
9
|
+
INVALID_CLASS_NAME_PREFIXES = /bg-|color-|text-|box-shadow-|text-|box_shadow-/.freeze
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def invalid?(class_name)
|
13
|
+
class_name.start_with?(INVALID_CLASS_NAME_PREFIXES) || Primer::Classify::Utilities.supported_selector?(class_name)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -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,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "primer/view_components/constants"
|
4
|
+
require "primer/classify/utilities"
|
5
|
+
require "primer/classify/validation"
|
6
|
+
require_relative "conversion_error"
|
7
|
+
require_relative "system_arguments"
|
8
|
+
require_relative "helpers/erb_block"
|
9
|
+
|
10
|
+
module ERBLint
|
11
|
+
module Linters
|
12
|
+
module ArgumentMappers
|
13
|
+
# Provides the base interface to implement an `ArgumentMapper`.
|
14
|
+
# Override attribute_to_args in a child class to customize its mapping behavior.
|
15
|
+
class Base
|
16
|
+
DEFAULT_TAG = nil
|
17
|
+
ATTRIBUTES = [].freeze
|
18
|
+
|
19
|
+
def initialize(tag)
|
20
|
+
@tag = tag
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
to_args.map { |k, v| "#{k}: #{v}" }.join(", ")
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_args
|
28
|
+
args = {}
|
29
|
+
|
30
|
+
args[:tag] = ":#{@tag.name}" unless self.class::DEFAULT_TAG.nil? || @tag.name == self.class::DEFAULT_TAG
|
31
|
+
|
32
|
+
@tag.attributes.each do |attribute|
|
33
|
+
attr_name = attribute.name
|
34
|
+
|
35
|
+
if self.class::ATTRIBUTES.include?(attr_name)
|
36
|
+
args.merge!(attribute_to_args(attribute))
|
37
|
+
elsif attr_name == "class"
|
38
|
+
args.merge!(map_classes(attribute))
|
39
|
+
else
|
40
|
+
# Assume the attribute is a system argument.
|
41
|
+
args.merge!(SystemArguments.new(attribute).to_args)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
args
|
46
|
+
end
|
47
|
+
|
48
|
+
def attribute_to_args(attribute); end
|
49
|
+
|
50
|
+
def map_classes(classes_node)
|
51
|
+
erb_helper.raise_if_erb_block(classes_node)
|
52
|
+
|
53
|
+
system_arguments = system_arguments_to_args(classes_node.value)
|
54
|
+
args = classes_to_args(system_arguments[:classes])
|
55
|
+
|
56
|
+
invalid_classes = args[:classes].select { |class_name| Primer::Classify::Validation.invalid?(class_name) }
|
57
|
+
|
58
|
+
raise ConversionError, "Cannot convert #{'class'.pluralize(invalid_classes.size)} #{invalid_classes.join(',')}" if invalid_classes.present?
|
59
|
+
|
60
|
+
# Using splat to order the arguments in Component's args -> System Args -> custom classes
|
61
|
+
res = {
|
62
|
+
**args.except(:classes),
|
63
|
+
**system_arguments.except(:classes)
|
64
|
+
}
|
65
|
+
|
66
|
+
if args[:classes].present?
|
67
|
+
res = {
|
68
|
+
**res,
|
69
|
+
classes: args[:classes].join(" ").to_json
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
res
|
74
|
+
end
|
75
|
+
|
76
|
+
# Override this with your component's mappings, it should return a hash with the component's arguments,
|
77
|
+
# including a `classes` key that will contain all classes that the mapper couldn't handle.
|
78
|
+
# @returns { classes: Array, ... }
|
79
|
+
def classes_to_args(classes)
|
80
|
+
{ classes: classes&.split(" ") || [] }
|
81
|
+
end
|
82
|
+
|
83
|
+
def system_arguments_to_args(classes)
|
84
|
+
system_arguments = Primer::Classify::Utilities.classes_to_hash(classes)
|
85
|
+
|
86
|
+
# need to transform symbols to strings with leading `:`
|
87
|
+
system_arguments.transform_values do |v|
|
88
|
+
v.is_a?(Symbol) ? ":#{v}" : v
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def erb_helper
|
95
|
+
@erb_helper ||= Helpers::ErbBlock.new
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -1,66 +1,53 @@
|
|
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
|
-
SCHEME_MAPPINGS =
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
"btn-link" => ":link"
|
17
|
-
}.freeze
|
9
|
+
class Button < Base
|
10
|
+
SCHEME_MAPPINGS = Primer::ViewComponents::Constants.get(
|
11
|
+
component: "Primer::ButtonComponent",
|
12
|
+
constant: "SCHEME_MAPPINGS",
|
13
|
+
symbolize: true
|
14
|
+
).freeze
|
18
15
|
|
19
|
-
VARIANT_MAPPINGS =
|
20
|
-
|
21
|
-
|
22
|
-
|
16
|
+
VARIANT_MAPPINGS = Primer::ViewComponents::Constants.get(
|
17
|
+
component: "Primer::ButtonComponent",
|
18
|
+
constant: "VARIANT_MAPPINGS",
|
19
|
+
symbolize: true
|
20
|
+
).freeze
|
23
21
|
|
24
|
-
TYPE_OPTIONS =
|
22
|
+
TYPE_OPTIONS = Primer::ViewComponents::Constants.get(
|
23
|
+
component: "Primer::BaseButton",
|
24
|
+
constant: "TYPE_OPTIONS"
|
25
|
+
).freeze
|
26
|
+
DEFAULT_TAG = Primer::ViewComponents::Constants.get(
|
27
|
+
component: "Primer::BaseButton",
|
28
|
+
constant: "DEFAULT_TAG"
|
29
|
+
).freeze
|
25
30
|
|
26
|
-
|
27
|
-
@tag = tag
|
28
|
-
end
|
29
|
-
|
30
|
-
def to_s
|
31
|
-
to_args.map { |k, v| "#{k}: #{v}" }.join(", ")
|
32
|
-
end
|
33
|
-
|
34
|
-
def to_args
|
35
|
-
args = {}
|
31
|
+
ATTRIBUTES = %w[disabled type].freeze
|
36
32
|
|
37
|
-
|
33
|
+
def attribute_to_args(attribute)
|
34
|
+
attr_name = attribute.name
|
38
35
|
|
39
|
-
|
40
|
-
|
36
|
+
case attr_name
|
37
|
+
when "disabled"
|
38
|
+
{ disabled: true }
|
39
|
+
when "type"
|
40
|
+
# button is the default type, so we don't need to do anything.
|
41
|
+
return {} if attribute.value == "button"
|
41
42
|
|
42
|
-
|
43
|
-
args = args.merge(classes_to_args(attribute))
|
44
|
-
elsif attr_name == "disabled"
|
45
|
-
args[:disabled] = true
|
46
|
-
elsif attr_name == "type"
|
47
|
-
# button is the default type, so we don't need to do anything.
|
48
|
-
next if attribute.value == "button"
|
43
|
+
raise ConversionError, "Button component does not support type \"#{attribute.value}\"" unless TYPE_OPTIONS.include?(attribute.value)
|
49
44
|
|
50
|
-
|
51
|
-
|
52
|
-
args[:type] = ":#{attribute.value}"
|
53
|
-
else
|
54
|
-
# Assume the attribute is a system argument.
|
55
|
-
args.merge!(SystemArguments.new(attribute).to_args)
|
56
|
-
end
|
45
|
+
{ type: ":#{attribute.value}" }
|
57
46
|
end
|
58
|
-
|
59
|
-
args
|
60
47
|
end
|
61
48
|
|
62
49
|
def classes_to_args(classes)
|
63
|
-
classes.
|
50
|
+
classes.split.each_with_object({ classes: [] }) do |class_name, acc|
|
64
51
|
next if class_name == "btn"
|
65
52
|
|
66
53
|
if SCHEME_MAPPINGS[class_name] && acc[:scheme].nil?
|
@@ -72,7 +59,7 @@ module ERBLint
|
|
72
59
|
elsif class_name == "BtnGroup-item"
|
73
60
|
acc[:group_item] = true
|
74
61
|
else
|
75
|
-
|
62
|
+
acc[:classes] << class_name
|
76
63
|
end
|
77
64
|
end
|
78
65
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module ERBLint
|
6
|
+
module Linters
|
7
|
+
module ArgumentMappers
|
8
|
+
# Maps attributes in the clipboard-copy element to arguments for the ClipboardCopy component.
|
9
|
+
class ClipboardCopy < Base
|
10
|
+
DEFAULT_TAG = "clipboard-copy"
|
11
|
+
ATTRIBUTES = %w[value].freeze
|
12
|
+
|
13
|
+
def attribute_to_args(attribute)
|
14
|
+
{ value: erb_helper.convert(attribute) }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../conversion_error"
|
4
|
+
|
5
|
+
module ERBLint
|
6
|
+
module Linters
|
7
|
+
module ArgumentMappers
|
8
|
+
module Helpers
|
9
|
+
# provides helpers to identify and deal with ERB blocks.
|
10
|
+
class ErbBlock
|
11
|
+
INTERPOLATION_REGEX = /^<%=(?<rb>.*)%>$/.freeze
|
12
|
+
|
13
|
+
def raise_if_erb_block(attribute)
|
14
|
+
raise_error(attribute) if any?(attribute)
|
15
|
+
end
|
16
|
+
|
17
|
+
def convert(attribute)
|
18
|
+
raise_error(attribute) unless interpolation?(attribute)
|
19
|
+
|
20
|
+
if any?(attribute)
|
21
|
+
convert_interpolation(attribute)
|
22
|
+
else
|
23
|
+
attribute.value.to_json
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def interpolation?(attribute)
|
30
|
+
erb_blocks(attribute).all? do |erb|
|
31
|
+
# If the blocks does not have an indicator, it's not an interpolation.
|
32
|
+
erb.children.to_a.compact.any? { |node| node.type == :indicator }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def raise_error(attribute)
|
37
|
+
raise ERBLint::Linters::ArgumentMappers::ConversionError, "Cannot convert attribute \"#{attribute.name}\" because its value contains an erb block"
|
38
|
+
end
|
39
|
+
|
40
|
+
def any?(attribute)
|
41
|
+
erb_blocks(attribute).any?
|
42
|
+
end
|
43
|
+
|
44
|
+
def basic?(attribute)
|
45
|
+
return false if erb_blocks(attribute).size != 1
|
46
|
+
|
47
|
+
attribute.value.match?(INTERPOLATION_REGEX)
|
48
|
+
end
|
49
|
+
|
50
|
+
def erb_blocks(attribute)
|
51
|
+
(attribute.value_node&.children || []).select { |n| n.try(:type) == :erb }
|
52
|
+
end
|
53
|
+
|
54
|
+
def convert_interpolation(attribute)
|
55
|
+
if basic?(attribute)
|
56
|
+
m = attribute.value.match(INTERPOLATION_REGEX)
|
57
|
+
return m[:rb].strip
|
58
|
+
end
|
59
|
+
|
60
|
+
# wrap the result in `""` so it is printed as a string
|
61
|
+
"\"#{attribute.value.gsub('<%=', '#{').gsub('%>', '}')}\""
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|