primer_view_components 0.0.47 → 0.0.51

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +113 -0
  3. data/app/components/primer/base_component.rb +2 -2
  4. data/app/components/primer/beta/auto_complete.rb +2 -2
  5. data/app/components/primer/beta/avatar.rb +1 -1
  6. data/app/components/primer/{avatar_stack_component.html.erb → beta/avatar_stack.html.erb} +0 -0
  7. data/app/components/primer/beta/avatar_stack.rb +92 -0
  8. data/app/components/primer/clipboard_copy.html.erb +2 -2
  9. data/app/components/primer/component.rb +5 -1
  10. data/app/components/primer/image_crop.html.erb +4 -4
  11. data/app/components/primer/label_component.rb +13 -12
  12. data/app/components/primer/navigation/tab_component.rb +30 -2
  13. data/app/components/primer/tab_nav_component.rb +4 -3
  14. data/app/components/primer/truncate.rb +1 -1
  15. data/app/components/primer/underline_nav_component.rb +3 -2
  16. data/app/lib/primer/octicon/cache.rb +1 -1
  17. data/lib/primer/classify.rb +0 -10
  18. data/lib/primer/classify/cache.rb +0 -5
  19. data/lib/primer/classify/utilities.rb +35 -12
  20. data/lib/primer/classify/utilities.yml +16 -0
  21. data/lib/primer/view_components.rb +34 -6
  22. data/lib/primer/view_components/constants.rb +55 -0
  23. data/lib/primer/view_components/linters/argument_mappers/base.rb +74 -0
  24. data/lib/primer/view_components/linters/argument_mappers/button.rb +32 -44
  25. data/lib/primer/view_components/linters/argument_mappers/clipboard_copy.rb +20 -0
  26. data/lib/primer/view_components/linters/argument_mappers/helpers/erb_block.rb +24 -0
  27. data/lib/primer/view_components/linters/argument_mappers/label.rb +50 -0
  28. data/lib/primer/view_components/linters/argument_mappers/system_arguments.rb +4 -1
  29. data/lib/primer/view_components/linters/autocorrectable.rb +30 -0
  30. data/lib/primer/view_components/linters/button_component_migration_counter.rb +9 -23
  31. data/lib/primer/view_components/linters/clipboard_copy_component_migration_counter.rb +21 -0
  32. data/lib/primer/view_components/linters/close_button_component_migration_counter.rb +16 -0
  33. data/lib/primer/view_components/linters/helpers.rb +42 -41
  34. data/lib/primer/view_components/linters/label_component_migration_counter.rb +25 -0
  35. data/lib/primer/view_components/version.rb +1 -1
  36. data/lib/rubocop/config/default.yml +12 -0
  37. data/lib/rubocop/cop/primer.rb +4 -0
  38. data/lib/rubocop/cop/primer/no_tag_memoize.rb +42 -0
  39. data/lib/rubocop/cop/primer/system_argument_instead_of_class.rb +85 -0
  40. data/lib/tasks/constants.rake +12 -0
  41. data/lib/tasks/docs.rake +24 -23
  42. data/lib/tasks/utilities.rake +4 -10
  43. data/lib/yard/docs_helper.rb +12 -3
  44. data/static/arguments.yml +973 -0
  45. data/static/assets/view-components.svg +18 -0
  46. data/static/classes.yml +174 -0
  47. data/static/constants.json +628 -0
  48. data/static/statuses.json +1 -1
  49. metadata +26 -8
  50. data/app/components/primer/avatar_stack_component.rb +0 -90
@@ -6,7 +6,7 @@ module Primer
6
6
  status :beta
7
7
 
8
8
  DEFAULT_TAG = :div
9
- TAG_OPTIONS = [DEFAULT_TAG, :span, :p].freeze
9
+ TAG_OPTIONS = [DEFAULT_TAG, :span, :p, :strong].freeze
10
10
 
11
11
  # @example Default
12
12
  # <div class="col-2">
@@ -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", :clippy, :clock, :"dot-fill", :info, :"kebab-horizontal", :link, :lock, :mail, :pencil, :plus, :question, :repo, :search, :"shield-lock", :star, :trash, :x].freeze
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)
@@ -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
- # Search each key/value_hash pair, eg. key `:mr` and value_hash `{ 0 => [ "mr-0", "mr-sm-0", "mr-md-0", "mr-lg-0", "mr-xl-0" ] }`
108
- UTILITIES.each do |key, value_hash|
109
- # Each value hash will also contain an array of classnames for breakpoints
110
- # Key argument `0`, classes `[ "mr-0", "mr-sm-0", "mr-md-0", "mr-lg-0", "mr-xl-0" ]`
111
- value_hash.each do |key_argument, classnames|
112
- # Skip each value hash until we get one with the selector
113
- next unless classnames.include?(selector)
114
-
115
- # Return [:mr, 0, 1]
116
- # has index of classname, so we can match it up with responsvie array `mr: [nil, 0]`
117
- return [key, key_argument, classnames.index(selector)]
118
- end
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
- DEFAULT_STATUSES_PATH = File.expand_path("static")
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
- statuses = Primer::Component.descendants.each_with_object({}) do |component, mem|
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: DEFAULT_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: DEFAULT_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 "conversion_error"
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
- "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
- VARIANT_MAPPINGS = {
20
- "btn-sm" => ":small",
21
- "btn-large" => ":large"
22
- }.freeze
12
+ SCHEME_MAPPINGS = Primer::ViewComponents::Constants.get(
13
+ component: "Primer::ButtonComponent",
14
+ constant: "SCHEME_MAPPINGS",
15
+ symbolize: true
16
+ ).freeze
23
17
 
24
- TYPE_OPTIONS = %w[button reset submit].freeze
18
+ VARIANT_MAPPINGS = Primer::ViewComponents::Constants.get(
19
+ component: "Primer::ButtonComponent",
20
+ constant: "VARIANT_MAPPINGS",
21
+ symbolize: true
22
+ ).freeze
25
23
 
26
- def initialize(tag)
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 = {}
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
- args[:tag] = ":#{@tag.name}" unless @tag.name == "button"
33
+ ATTRIBUTES = %w[disabled type].freeze
38
34
 
39
- @tag.attributes.each do |attribute|
40
- attr_name = attribute.name
35
+ def attribute_to_args(attribute)
36
+ attr_name = attribute.name
41
37
 
42
- if attr_name == "class"
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"
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
- raise ConversionError, "Button component does not support type \"#{attribute.value}\"" unless TYPE_OPTIONS.include?(attribute.value)
44
+ raise ConversionError, "Button component does not support type \"#{attribute.value}\"" unless TYPE_OPTIONS.include?(attribute.value)
51
45
 
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
46
+ { type: ":#{attribute.value}" }
57
47
  end
58
-
59
- args
60
48
  end
61
49
 
62
50
  def classes_to_args(classes)
63
- classes.value.split(" ").each_with_object({}) do |class_name, acc|
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?