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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +155 -0
  3. data/app/components/primer/base_component.rb +2 -2
  4. data/app/components/primer/beta/avatar.rb +1 -1
  5. data/app/components/primer/{avatar_stack_component.html.erb → beta/avatar_stack.html.erb} +0 -0
  6. data/app/components/primer/beta/avatar_stack.rb +92 -0
  7. data/app/components/primer/beta/truncate.html.erb +5 -0
  8. data/app/components/primer/beta/truncate.rb +110 -0
  9. data/app/components/primer/border_box_component.rb +27 -1
  10. data/app/components/primer/clipboard_copy.html.erb +2 -2
  11. data/app/components/primer/clipboard_copy.rb +1 -1
  12. data/app/components/primer/dropdown.rb +7 -7
  13. data/app/components/primer/icon_button.rb +1 -1
  14. data/app/components/primer/image_crop.html.erb +4 -4
  15. data/app/components/primer/label_component.rb +13 -12
  16. data/app/components/primer/navigation/tab_component.rb +16 -2
  17. data/app/components/primer/progress_bar_component.rb +0 -3
  18. data/app/components/primer/tab_nav_component.rb +4 -3
  19. data/app/components/primer/truncate.rb +1 -1
  20. data/app/components/primer/underline_nav_component.rb +3 -2
  21. data/app/lib/primer/fetch_or_fallback_helper.rb +2 -0
  22. data/app/lib/primer/octicon/cache.rb +1 -1
  23. data/app/lib/primer/tabbed_component_helper.rb +1 -1
  24. data/app/lib/primer/view_helper.rb +1 -0
  25. data/lib/primer/classify.rb +4 -16
  26. data/lib/primer/classify/cache.rb +0 -5
  27. data/lib/primer/classify/flex.rb +1 -1
  28. data/lib/primer/classify/functional_colors.rb +1 -1
  29. data/lib/primer/classify/utilities.rb +51 -13
  30. data/lib/primer/classify/utilities.yml +16 -0
  31. data/lib/primer/classify/validation.rb +18 -0
  32. data/lib/primer/view_components.rb +34 -6
  33. data/lib/primer/view_components/constants.rb +55 -0
  34. data/lib/primer/view_components/linters/argument_mappers/base.rb +100 -0
  35. data/lib/primer/view_components/linters/argument_mappers/button.rb +33 -46
  36. data/lib/primer/view_components/linters/argument_mappers/clipboard_copy.rb +19 -0
  37. data/lib/primer/view_components/linters/argument_mappers/helpers/erb_block.rb +67 -0
  38. data/lib/primer/view_components/linters/argument_mappers/label.rb +49 -0
  39. data/lib/primer/view_components/linters/argument_mappers/system_arguments.rb +6 -5
  40. data/lib/primer/view_components/linters/autocorrectable.rb +30 -0
  41. data/lib/primer/view_components/linters/button_component_migration_counter.rb +9 -23
  42. data/lib/primer/view_components/linters/clipboard_copy_component_migration_counter.rb +21 -0
  43. data/lib/primer/view_components/linters/close_button_component_migration_counter.rb +16 -0
  44. data/lib/primer/view_components/linters/helpers.rb +47 -42
  45. data/lib/primer/view_components/linters/label_component_migration_counter.rb +25 -0
  46. data/lib/primer/view_components/version.rb +1 -1
  47. data/lib/rubocop/config/default.yml +5 -0
  48. data/lib/rubocop/cop/primer.rb +1 -2
  49. data/lib/rubocop/cop/primer/deprecated_arguments.rb +173 -0
  50. data/lib/rubocop/cop/primer/no_tag_memoize.rb +1 -0
  51. data/lib/rubocop/cop/primer/primer_octicon.rb +178 -0
  52. data/lib/rubocop/cop/primer/system_argument_instead_of_class.rb +12 -16
  53. data/lib/tasks/constants.rake +12 -0
  54. data/lib/tasks/coverage.rake +4 -0
  55. data/lib/tasks/docs.rake +27 -25
  56. data/lib/tasks/utilities.rake +9 -13
  57. data/lib/yard/docs_helper.rb +15 -5
  58. data/static/arguments.yml +980 -0
  59. data/static/assets/view-components.svg +18 -0
  60. data/static/classes.yml +182 -0
  61. data/static/constants.json +640 -0
  62. data/static/statuses.json +4 -2
  63. metadata +29 -10
  64. data/app/components/primer/avatar_stack_component.rb +0 -90
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "rubocop"
4
4
 
5
+ # :nocov:
5
6
  module RuboCop
6
7
  module Cop
7
8
  module Primer
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+ require "primer/classify/utilities"
5
+ require "primer/classify/validation"
6
+
7
+ # :nocov:
8
+ module RuboCop
9
+ module Cop
10
+ module Primer
11
+ # This cop ensures that components use System Arguments instead of CSS classes.
12
+ #
13
+ # bad
14
+ # octicon(:icon)
15
+ # octicon("icon")
16
+ # octicon("icon-with-daashes")
17
+ # octicon(@ivar)
18
+ # octicon(condition > "icon" : "other-icon")
19
+ #
20
+ # good
21
+ # primer_octicon(:icon)
22
+ # primer_octicon(:"icon-with-daashes")
23
+ # primer_octicon(@ivar)
24
+ # primer_octicon(condition > "icon" : "other-icon")
25
+ class PrimerOcticon < RuboCop::Cop::Cop
26
+ INVALID_MESSAGE = <<~STR
27
+ Replace the octicon helper with primer_octicon. See https://primer.style/view-components/components/octicon for details.
28
+ STR
29
+
30
+ SIZE_ATTRIBUTES = %w[height width size].freeze
31
+ STRING_ATTRIBUTES = %w[aria- data-].freeze
32
+ VALID_ATTRIBUTES = [*SIZE_ATTRIBUTES, *STRING_ATTRIBUTES, "class"].freeze
33
+
34
+ STRING_ATTRIBUTE_REGEX = Regexp.union(STRING_ATTRIBUTES).freeze
35
+ ATTRIBUTE_REGEX = Regexp.union(VALID_ATTRIBUTES).freeze
36
+ INVALID_ATTRIBUTE = -1
37
+
38
+ def on_send(node)
39
+ return unless node.method_name == :octicon
40
+ return unless node.arguments?
41
+
42
+ kwargs = kwargs(node)
43
+
44
+ return unless kwargs.type == :hash
45
+
46
+ attributes = kwargs.keys.map(&:value)
47
+
48
+ # Don't convert unknown attributes
49
+ return unless attributes.all? { |attribute| attribute.match?(ATTRIBUTE_REGEX) }
50
+ # Can't convert size
51
+ return if octicon_size_attributes(kwargs) == INVALID_ATTRIBUTE
52
+
53
+ # find class pair
54
+ classes = classes(kwargs)
55
+
56
+ return if classes == INVALID_ATTRIBUTE
57
+
58
+ # check if classes are convertible
59
+ if classes.present?
60
+ system_arguments = ::Primer::Classify::Utilities.classes_to_hash(classes)
61
+ invalid_classes = (system_arguments[:classes]&.split(" ") || []).select { |class_name| ::Primer::Classify::Validation.invalid?(class_name) }
62
+
63
+ # Uses system argument that can't be converted
64
+ return if invalid_classes.present?
65
+ end
66
+
67
+ add_offense(node, message: INVALID_MESSAGE)
68
+ end
69
+
70
+ def autocorrect(node)
71
+ lambda do |corrector|
72
+ kwargs = kwargs(node)
73
+
74
+ # Converting arguments for the component
75
+ classes = classes(kwargs)
76
+ size_attributes = transform_sizes(kwargs)
77
+ args = arguments_as_string(node, size_attributes, classes)
78
+
79
+ corrector.replace(node.loc.expression, "primer_octicon(#{args})")
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def transform_sizes(kwargs)
86
+ attributes = octicon_size_attributes(kwargs)
87
+
88
+ attributes.transform_values do |size|
89
+ if size.between?(10, 16)
90
+ ""
91
+ elsif size.between?(22, 26)
92
+ ":medium"
93
+ else
94
+ size
95
+ end
96
+ end
97
+ end
98
+
99
+ def octicon_size_attributes(kwargs)
100
+ kwargs.pairs.each_with_object({}) do |pair, h|
101
+ next unless SIZE_ATTRIBUTES.include?(pair.key.value.to_s)
102
+
103
+ # We only support string or int values.
104
+ case pair.value.type
105
+ when :int
106
+ h[pair.key.value] = pair.value.source.to_i
107
+ when :str
108
+ h[pair.key.value] = pair.value.value.to_i
109
+ else
110
+ return INVALID_ATTRIBUTE
111
+ end
112
+ end
113
+ end
114
+
115
+ def classes(kwargs)
116
+ # find class pair
117
+ class_arg = kwargs.pairs.find { |kwarg| kwarg.key.value == :class }
118
+
119
+ return if class_arg.blank?
120
+ return INVALID_ATTRIBUTE unless class_arg.value.type == :str
121
+
122
+ class_arg.value.value
123
+ end
124
+
125
+ def arguments_as_string(node, size_attributes, classes)
126
+ args = icon(node.arguments.first)
127
+ size_args = size_attributes_to_string(size_attributes)
128
+ string_args = string_args_to_string(node)
129
+
130
+ args = "#{args}, #{size_attributes_to_string(size_attributes)}" if size_args.present?
131
+ args = "#{args}, #{::Primer::Classify::Utilities.classes_to_args(classes)}" if classes.present?
132
+ args = "#{args}, #{string_args}" if string_args.present?
133
+
134
+ args
135
+ end
136
+
137
+ def size_attributes_to_string(size_attributes)
138
+ # No arguments if they map to the default size
139
+ return if size_attributes.blank? || size_attributes.values.all?(&:blank?)
140
+ # Return mapped argument to `size`
141
+ return "size: :medium" if size_attributes.values.any?(":medium")
142
+
143
+ size_attributes.map do |key, value|
144
+ "#{key}: #{value}"
145
+ end.join(", ")
146
+ end
147
+
148
+ def string_args_to_string(node)
149
+ kwargs = kwargs(node)
150
+
151
+ args = kwargs.pairs.each_with_object([]) do |pair, acc|
152
+ next unless pair.key.value.to_s.match?(STRING_ATTRIBUTE_REGEX)
153
+
154
+ key = pair.key.value.to_s == "data-test-selector" ? "test_selector" : "\"#{pair.key.value}\""
155
+ acc << "#{key}: #{pair.value.source}"
156
+ end
157
+
158
+ args.join(",")
159
+ end
160
+
161
+ def kwargs(node)
162
+ return node.arguments.last if node.arguments.size > 1
163
+
164
+ OpenStruct.new(keys: [], pairs: [], type: :hash)
165
+ end
166
+
167
+ def icon(node)
168
+ return node.source unless node.type == :str
169
+ return ":#{node.value}" unless node.value.include?("-")
170
+
171
+ # If the icon contains `-` we need to cast the string as a symbole
172
+ # E.g: `arrow-down` becomes `:"arrow-down"`
173
+ ":#{node.source}"
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
@@ -3,7 +3,9 @@
3
3
  require "rubocop"
4
4
  require "primer/classify/utilities"
5
5
  require "primer/view_components/statuses"
6
+ require_relative "../../../../app/lib/primer/view_helper"
6
7
 
8
+ # :nocov:
7
9
  module RuboCop
8
10
  module Cop
9
11
  module Primer
@@ -20,8 +22,7 @@ module RuboCop
20
22
  STR
21
23
 
22
24
  def on_send(node)
23
- return unless node.method_name == :new
24
- return unless ::Primer::ViewComponents::STATUSES.key?(node.receiver.const_name)
25
+ return unless valid_node?(node)
25
26
  return unless node.arguments?
26
27
 
27
28
  # we are looking for hash arguments and they are always last
@@ -48,26 +49,21 @@ module RuboCop
48
49
 
49
50
  def autocorrect(node)
50
51
  lambda do |corrector|
51
- system_arguments = ::Primer::Classify::Utilities.classes_to_hash(node.value.value)
52
- corrector.replace(node.loc.expression, arguments_as_string(system_arguments))
52
+ args = ::Primer::Classify::Utilities.classes_to_args(node.value.value)
53
+ corrector.replace(node.loc.expression, args)
53
54
  end
54
55
  end
55
56
 
56
57
  private
57
58
 
58
- def arguments_as_string(system_arguments)
59
- system_arguments.map do |key, value|
60
- val = case value
61
- when Symbol
62
- ":#{value}"
63
- when String
64
- value.to_json
65
- else
66
- value
67
- end
59
+ # We only verify SystemArguments if it's a `.new` call on a component or
60
+ # a ViewHleper call.
61
+ def valid_node?(node)
62
+ view_helpers.include?(node.method_name) || (node.method_name == :new && ::Primer::ViewComponents::STATUSES.key?(node.receiver.const_name))
63
+ end
68
64
 
69
- "#{key}: #{val}"
70
- end.join(", ")
65
+ def view_helpers
66
+ ::Primer::ViewHelper::HELPERS.keys.map { |key| "primer_#{key}".to_sym }
71
67
  end
72
68
  end
73
69
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :constants do
4
+ task :dump do
5
+ require File.expand_path("./../../demo/config/environment.rb", __dir__)
6
+ require "primer/view_components"
7
+ # Loads all components for `.descendants` to work properly
8
+ Dir["./app/components/primer/**/*.rb"].sort.each { |file| require file }
9
+
10
+ Primer::ViewComponents.dump_constants
11
+ end
12
+ end
@@ -9,6 +9,10 @@ namespace :coverage do
9
9
 
10
10
  SimpleCov.collate Dir["simplecov-resultset-*/.resultset.json"], "rails" do
11
11
  formatter SimpleCov::Formatter::Console
12
+
13
+ add_group "Ignored Code" do |src_file|
14
+ File.readlines(src_file.filename).grep(/:nocov:/).any?
15
+ end
12
16
  end
13
17
  end
14
18
  end
data/lib/tasks/docs.rake CHANGED
@@ -35,7 +35,7 @@ namespace :docs do
35
35
  Primer::Beta::AutoComplete,
36
36
  Primer::Beta::AutoComplete::Item,
37
37
  Primer::Beta::Avatar,
38
- Primer::AvatarStackComponent,
38
+ Primer::Beta::AvatarStack,
39
39
  Primer::BaseButton,
40
40
  Primer::BlankslateComponent,
41
41
  Primer::BorderBoxComponent,
@@ -74,6 +74,7 @@ namespace :docs do
74
74
  Primer::TimelineItemComponent,
75
75
  Primer::Tooltip,
76
76
  Primer::Truncate,
77
+ Primer::Beta::Truncate,
77
78
  Primer::UnderlineNavComponent
78
79
  ]
79
80
 
@@ -110,6 +111,7 @@ namespace :docs do
110
111
  File.open(path, "w") do |f|
111
112
  f.puts("---")
112
113
  f.puts("title: #{data[:title]}")
114
+ f.puts("componentId: #{data[:component_id]}")
113
115
  f.puts("status: #{data[:status]}")
114
116
  f.puts("source: #{data[:source]}")
115
117
  f.puts("storybook: #{data[:storybook]}")
@@ -227,28 +229,17 @@ namespace :docs do
227
229
  f.puts("## Examples")
228
230
 
229
231
  initialize_method.tags(:example).each do |tag|
230
- name = tag.name
231
- description = nil
232
- code = nil
233
-
234
- if tag.text.include?("@description")
235
- splitted = tag.text.split(/@description|@code/)
236
- description = splitted.second.gsub(/^[ \t]{2}/, "").strip
237
- code = splitted.last.gsub(/^[ \t]{2}/, "").strip
238
- else
239
- code = tag.text
240
- end
241
-
232
+ name, description, code = parse_example_tag(tag)
242
233
  f.puts
243
234
  f.puts("### #{name}")
244
235
  if description
245
236
  f.puts
246
- f.puts(description)
237
+ f.puts(view_context.render(inline: description.squish))
247
238
  end
248
239
  f.puts
249
240
  html = view_context.render(inline: code)
250
241
  html.scan(/class="([^"]*)"/) do |classnames|
251
- classes_found_in_examples.concat(classnames[0].split(" ").reject { |c| c.starts_with?("octicon", "js", "my-") }.map { ".#{_1}"})
242
+ classes_found_in_examples.concat(classnames[0].split.reject { |c| c.starts_with?("octicon", "js", "my-") }.map { ".#{_1}" })
252
243
  end
253
244
  f.puts("<Example src=\"#{html.tr('"', "\'").delete("\n")}\" />")
254
245
  f.puts
@@ -330,13 +321,14 @@ namespace :docs do
330
321
  f.puts(" class #{short_name}Preview < ViewComponent::Preview")
331
322
 
332
323
  yard_example_tags.each_with_index do |tag, index|
333
- method_name = tag.name.split("|").first.downcase.parameterize.underscore
324
+ name, _, code = parse_example_tag(tag)
325
+ method_name = name.split("|").first.downcase.parameterize.underscore
334
326
  f.puts(" def #{method_name}; end")
335
327
  f.puts unless index == yard_example_tags.size - 1
336
328
  path = Pathname.new("demo/test/components/previews/primer/docs/#{short_name.underscore}_preview/#{method_name}.html.erb")
337
329
  path.dirname.mkdir unless path.dirname.exist?
338
330
  File.open(path, "w") do |view_file|
339
- view_file.puts(tag.text.to_s)
331
+ view_file.puts(code.to_s)
340
332
  end
341
333
  end
342
334
 
@@ -374,8 +366,24 @@ namespace :docs do
374
366
  registry
375
367
  end
376
368
 
369
+ def parse_example_tag(tag)
370
+ name = tag.name
371
+ description = nil
372
+ code = nil
373
+
374
+ if tag.text.include?("@description")
375
+ splitted = tag.text.split(/@description|@code/)
376
+ description = splitted.second.gsub(/^[ \t]{2}/, "").strip
377
+ code = splitted.last.gsub(/^[ \t]{2}/, "").strip
378
+ else
379
+ code = tag.text
380
+ end
381
+
382
+ [name, description, code]
383
+ end
384
+
377
385
  def pretty_default_value(tag, component)
378
- params = tag.object.parameters.find { |param| [tag.name.to_s, tag.name.to_s + ":"].include?(param[0]) }
386
+ params = tag.object.parameters.find { |param| [tag.name.to_s, "#{tag.name}:"].include?(param[0]) }
379
387
  default = tag.defaults&.first || params&.second
380
388
 
381
389
  return "N/A" unless default
@@ -388,13 +396,6 @@ namespace :docs do
388
396
  pretty_value(constant_value)
389
397
  end
390
398
 
391
- def status_module_and_short_name(component)
392
- name_with_status = component.name.gsub(/Primer::|Component/, "")
393
-
394
- m = name_with_status.match(/(?<status>Beta|Alpha|Deprecated)?(::)?(?<name>.*)/)
395
- [m[:status]&.downcase, m[:name].gsub("::", "")]
396
- end
397
-
398
399
  def docs_metadata(component)
399
400
  (status_module, short_name) = status_module_and_short_name(component)
400
401
  status_path = status_module.nil? ? "" : "#{status_module}/"
@@ -402,6 +403,7 @@ namespace :docs do
402
403
 
403
404
  {
404
405
  title: short_name,
406
+ component_id: short_name.underscore,
405
407
  status: status.capitalize,
406
408
  source: source_url(component),
407
409
  storybook: storybook_url(component),
@@ -5,30 +5,26 @@ namespace :utilities do
5
5
  require "yaml"
6
6
  require "json"
7
7
  require File.expand_path("./../../demo/config/environment.rb", __dir__)
8
+ require "primer/classify/utilities"
8
9
 
9
10
  # Keys that are looked for to be included in the utilities.yml file
11
+ # rubocop:disable Lint/ConstantDefinitionInBlock
10
12
  SUPPORTED_KEYS = %i[
11
13
  anim
12
14
  d
13
15
  float
16
+ height
14
17
  hide
15
18
  m mt mr mb ml mx my
16
19
  p pt pr pb pl px py
17
20
  position
18
21
  wb
22
+ width
19
23
  v
20
24
  ].freeze
21
25
 
22
- # Replacements for some classnames that end up being a different argument key
23
- REPLACEMENT_KEYS = {
24
- "^anim" => "animation",
25
- "^v-align" => "vertical_align",
26
- "^d" => "display",
27
- "^wb" => "word_break",
28
- "^v" => "visibility"
29
- }.freeze
30
-
31
26
  BREAKPOINTS = [nil, "sm", "md", "lg", "xl"].freeze
27
+ # rubocop:enable Lint/ConstantDefinitionInBlock
32
28
 
33
29
  css_data =
34
30
  JSON.parse(
@@ -44,7 +40,7 @@ namespace :utilities do
44
40
  css_data.each do |selector|
45
41
  selector.sub!(/^./, "")
46
42
  # Next if selector has ancestors or sibling selectors
47
- next if selector.match?(/[:><~\[\.]/)
43
+ next if selector.match?(/[:><~\[.]/)
48
44
  next unless SUPPORTED_KEYS.any? { |key| selector.start_with?("#{key}-") }
49
45
 
50
46
  # Dupe so we still have the selector at the end of slicing it up
@@ -52,11 +48,11 @@ namespace :utilities do
52
48
  key = ""
53
49
 
54
50
  # Look for a replacement key
55
- REPLACEMENT_KEYS.each do |k, v|
51
+ Primer::Classify::Utilities::REPLACEMENT_KEYS.each do |k, v|
56
52
  next unless classname.match?(Regexp.new(k))
57
53
 
58
54
  key = v
59
- classname.sub!(Regexp.new(k + "-"), "")
55
+ classname.sub!(Regexp.new("#{k}-"), "")
60
56
  end
61
57
 
62
58
  # If we didn't find a replacement, grab the first text before hyphen
@@ -72,7 +68,7 @@ namespace :utilities do
72
68
  end
73
69
 
74
70
  # Change the rest from hypens to underscores
75
- classname.sub!(/\-/, "_")
71
+ classname.sub!(/-/, "_")
76
72
 
77
73
  # convert padding/margin negative values ie n7 to -7
78
74
  classname.sub!(/^n/, "-") if classname.match?(/^n[0-9]/)