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.
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?