primer_view_components 0.0.48 → 0.0.52

Sign up to get free protection for your applications and to get access to all the features.
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]/)