primer_view_components 0.0.46 → 0.0.50

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +144 -0
  3. data/app/components/primer/base_component.rb +2 -2
  4. data/app/components/primer/beta/auto_complete.rb +159 -0
  5. data/app/components/primer/beta/auto_complete/auto_complete.d.ts +1 -0
  6. data/app/components/primer/{auto_complete → beta/auto_complete}/auto_complete.html.erb +0 -0
  7. data/app/components/primer/beta/auto_complete/auto_complete.js +1 -0
  8. data/app/components/primer/{auto_complete → beta/auto_complete}/auto_complete.ts +0 -0
  9. data/app/components/primer/beta/auto_complete/item.rb +44 -0
  10. data/app/components/primer/beta/avatar.rb +77 -0
  11. data/app/components/primer/{avatar_stack_component.html.erb → beta/avatar_stack.html.erb} +0 -0
  12. data/app/components/primer/beta/avatar_stack.rb +92 -0
  13. data/app/components/primer/clipboard_copy.html.erb +2 -2
  14. data/app/components/primer/component.rb +5 -1
  15. data/app/components/primer/details_component.rb +7 -7
  16. data/app/components/primer/image_crop.html.erb +4 -4
  17. data/app/components/primer/label_component.rb +13 -12
  18. data/app/components/primer/markdown.rb +9 -9
  19. data/app/components/primer/navigation/tab_component.rb +30 -2
  20. data/app/components/primer/popover_component.rb +6 -3
  21. data/app/components/primer/primer.d.ts +1 -1
  22. data/app/components/primer/primer.js +1 -1
  23. data/app/components/primer/primer.ts +1 -1
  24. data/app/components/primer/tab_nav_component.rb +4 -3
  25. data/app/components/primer/timeline_item_component.rb +2 -2
  26. data/app/components/primer/truncate.rb +6 -1
  27. data/app/components/primer/underline_nav_component.rb +4 -3
  28. data/app/lib/primer/octicon/cache.rb +1 -1
  29. data/lib/primer/classify.rb +4 -18
  30. data/lib/primer/classify/cache.rb +0 -5
  31. data/lib/primer/classify/utilities.rb +54 -22
  32. data/lib/primer/classify/utilities.yml +16 -0
  33. data/lib/primer/view_components.rb +34 -6
  34. data/lib/primer/view_components/constants.rb +55 -0
  35. data/lib/primer/view_components/linters/argument_mappers/base.rb +74 -0
  36. data/lib/primer/view_components/linters/argument_mappers/button.rb +32 -44
  37. data/lib/primer/view_components/linters/argument_mappers/clipboard_copy.rb +20 -0
  38. data/lib/primer/view_components/linters/argument_mappers/helpers/erb_block.rb +24 -0
  39. data/lib/primer/view_components/linters/argument_mappers/label.rb +50 -0
  40. data/lib/primer/view_components/linters/argument_mappers/system_arguments.rb +4 -1
  41. data/lib/primer/view_components/linters/autocorrectable.rb +30 -0
  42. data/lib/primer/view_components/linters/button_component_migration_counter.rb +9 -23
  43. data/lib/primer/view_components/linters/clipboard_copy_component_migration_counter.rb +21 -0
  44. data/lib/primer/view_components/linters/close_button_component_migration_counter.rb +16 -0
  45. data/lib/primer/view_components/linters/helpers.rb +56 -38
  46. data/lib/primer/view_components/linters/label_component_migration_counter.rb +25 -0
  47. data/lib/primer/view_components/statuses.rb +14 -0
  48. data/lib/primer/view_components/version.rb +1 -1
  49. data/lib/rubocop/config/default.yml +12 -0
  50. data/lib/rubocop/cop/primer.rb +4 -0
  51. data/lib/rubocop/cop/primer/no_tag_memoize.rb +42 -0
  52. data/lib/rubocop/cop/primer/system_argument_instead_of_class.rb +75 -0
  53. data/lib/tasks/constants.rake +12 -0
  54. data/lib/tasks/docs.rake +87 -32
  55. data/lib/tasks/utilities.rake +4 -10
  56. data/lib/yard/docs_helper.rb +12 -3
  57. data/static/arguments.yml +973 -0
  58. data/static/assets/view-components.svg +18 -0
  59. data/static/classes.yml +174 -0
  60. data/static/constants.json +628 -0
  61. data/static/statuses.json +5 -5
  62. metadata +34 -13
  63. data/app/components/primer/auto_complete.rb +0 -157
  64. data/app/components/primer/auto_complete/item.rb +0 -42
  65. data/app/components/primer/avatar_component.rb +0 -75
  66. data/app/components/primer/avatar_stack_component.rb +0 -90
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "helpers"
4
+ require_relative "autocorrectable"
5
+ require_relative "argument_mappers/label"
6
+
7
+ module ERBLint
8
+ module Linters
9
+ # Counts the number of times a HTML label is used instead of the component.
10
+ class LabelComponentMigrationCounter < Linter
11
+ include Helpers
12
+ include Autocorrectable
13
+
14
+ TAGS = Primer::ViewComponents::Constants.get(
15
+ component: "Primer::LabelComponent",
16
+ constant: "TAG_OPTIONS"
17
+ ).freeze
18
+
19
+ CLASSES = %w[Label].freeze
20
+ MESSAGE = "We are migrating labels to use [Primer::LabelComponent](https://primer.style/view-components/components/label), please try to use that instead of raw HTML."
21
+ ARGUMENT_MAPPER = ArgumentMappers::Label
22
+ COMPONENT = "Primer::LabelComponent"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Primer
6
+ # :nodoc:
7
+ module ViewComponents
8
+ STATUSES = JSON.parse(
9
+ File.read(
10
+ File.join(File.dirname(__FILE__), "../../../static/statuses.json")
11
+ )
12
+ ).freeze
13
+ end
14
+ end
@@ -5,7 +5,7 @@ module Primer
5
5
  module VERSION
6
6
  MAJOR = 0
7
7
  MINOR = 0
8
- PATCH = 46
8
+ PATCH = 50
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH].join(".")
11
11
  end
@@ -0,0 +1,12 @@
1
+ require:
2
+ - rubocop/cop/primer
3
+
4
+ AllCops:
5
+ DisabledByDefault: true
6
+
7
+ Primer/SystemArgumentInsteadOfClass:
8
+ Enabled: true
9
+
10
+ Primer/NoTagMemoize:
11
+ Enabled: false
12
+
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop/cop/primer/no_tag_memoize"
4
+ require "rubocop/cop/primer/system_argument_instead_of_class"
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Primer
8
+ # This cop ensures that tags are not set with ||=
9
+ #
10
+ # bad
11
+ # @system_arguments[:tag] ||= :h1
12
+ #
13
+ # good
14
+ # @system_arguments[:tag] = fetch_or_fallback(TAG_OPTIONS, tag, DEFAULT_TAG)
15
+ #
16
+ # good
17
+ # @system_arguments[:tag] = :h2
18
+ class NoTagMemoize < RuboCop::Cop::Cop
19
+ INVALID_MESSAGE = <<~STR
20
+ Avoid `[:tag] ||=`. Instead, try one of the following:
21
+ - Don't allow consumers to update the tag by having a fixed tag (e.g. `system_arguments[:tag] = :div`)
22
+ - Use the `fetch_or_fallback` helper to only allow a tag from a restricted list.
23
+ STR
24
+
25
+ def_node_search :tag_memoized?, <<~PATTERN
26
+ (or-asgn
27
+ (send
28
+ _
29
+ _
30
+ (sym :tag)
31
+ )
32
+ _
33
+ )
34
+ PATTERN
35
+
36
+ def on_or_asgn(node)
37
+ add_offense(node, message: INVALID_MESSAGE) if tag_memoized?(node)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+ require "primer/classify/utilities"
5
+ require "primer/view_components/statuses"
6
+
7
+ module RuboCop
8
+ module Cop
9
+ module Primer
10
+ # This cop ensures that components use System Arguments instead of CSS classes.
11
+ #
12
+ # bad
13
+ # Component.new(classes: "mr-1")
14
+ #
15
+ # good
16
+ # Component.new(mr: 1)
17
+ class SystemArgumentInsteadOfClass < RuboCop::Cop::Cop
18
+ INVALID_MESSAGE = <<~STR
19
+ Avoid using CSS classes when you can use System Arguments: https://primer.style/view-components/system-arguments.
20
+ STR
21
+
22
+ 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 node.arguments?
26
+
27
+ # we are looking for hash arguments and they are always last
28
+ kwargs = node.arguments.last
29
+
30
+ return unless kwargs.type == :hash
31
+
32
+ # find classes pair
33
+ classes_arg = kwargs.pairs.find { |kwarg| kwarg.key.value == :classes }
34
+
35
+ return if classes_arg.nil?
36
+ return unless classes_arg.value.type == :str
37
+
38
+ # get actual classes
39
+ classes = classes_arg.value.value
40
+
41
+ system_arguments = ::Primer::Classify::Utilities.classes_to_hash(classes)
42
+
43
+ # no classes are fixable
44
+ return if system_arguments[:classes] == classes
45
+
46
+ add_offense(classes_arg, message: INVALID_MESSAGE)
47
+ end
48
+
49
+ def autocorrect(node)
50
+ 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))
53
+ end
54
+ end
55
+
56
+ private
57
+
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
68
+
69
+ "#{key}: #{val}"
70
+ end.join(", ")
71
+ end
72
+ end
73
+ end
74
+ end
75
+ 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
data/lib/tasks/docs.rake CHANGED
@@ -32,10 +32,10 @@ namespace :docs do
32
32
  Primer::OcticonSymbolsComponent,
33
33
  Primer::ImageCrop,
34
34
  Primer::IconButton,
35
- Primer::AutoComplete,
36
- Primer::AutoComplete::Item,
37
- Primer::AvatarComponent,
38
- Primer::AvatarStackComponent,
35
+ Primer::Beta::AutoComplete,
36
+ Primer::Beta::AutoComplete::Item,
37
+ Primer::Beta::Avatar,
38
+ Primer::Beta::AvatarStack,
39
39
  Primer::BaseButton,
40
40
  Primer::BlankslateComponent,
41
41
  Primer::BorderBoxComponent,
@@ -81,7 +81,7 @@ namespace :docs do
81
81
  Primer::Dropdown,
82
82
  Primer::LocalTime,
83
83
  Primer::ImageCrop,
84
- Primer::AutoComplete,
84
+ Primer::Beta::AutoComplete,
85
85
  Primer::ClipboardCopy,
86
86
  Primer::TabContainerComponent,
87
87
  Primer::TabNavComponent,
@@ -97,28 +97,31 @@ namespace :docs do
97
97
 
98
98
  errors = []
99
99
 
100
- components.each do |component|
100
+ # Deletes docs before regenerating them, guaranteeing that we don't keep stale docs.
101
+ FileUtils.rm_rf(Dir.glob("docs/content/components/**/*.md"))
102
+
103
+ components.sort_by(&:name).each do |component|
101
104
  documentation = registry.get(component.name)
102
105
 
103
- # Primer::AvatarComponent => Avatar
104
- short_name = component.name.gsub(/Primer|::|Component/, "")
106
+ data = docs_metadata(component)
105
107
 
106
- path = Pathname.new("docs/content/components/#{short_name.downcase}.md")
108
+ path = Pathname.new(data[:path])
107
109
  path.dirname.mkdir unless path.dirname.exist?
108
110
  File.open(path, "w") do |f|
109
111
  f.puts("---")
110
- f.puts("title: #{short_name}")
111
- f.puts("status: #{component.status.to_s.capitalize}")
112
- f.puts("source: https://github.com/primer/view_components/tree/main/app/components/primer/#{component.to_s.demodulize.underscore}.rb")
113
- f.puts("storybook: https://primer.style/view-components/stories/?path=/story/primer-#{short_name.underscore.dasherize}-component")
112
+ f.puts("title: #{data[:title]}")
113
+ f.puts("componentId: #{data[:component_id]}")
114
+ f.puts("status: #{data[:status]}")
115
+ f.puts("source: #{data[:source]}")
116
+ f.puts("storybook: #{data[:storybook]}")
114
117
  f.puts("---")
115
118
  f.puts
116
- f.puts("import Example from '../../src/@primer/gatsby-theme-doctocat/components/example'")
119
+ f.puts("import Example from '#{data[:example_path]}'")
117
120
 
118
121
  initialize_method = documentation.meths.find(&:constructor?)
119
122
 
120
123
  if js_components.include?(component)
121
- f.puts("import RequiresJSFlash from '../../src/@primer/gatsby-theme-doctocat/components/requires-js-flash'")
124
+ f.puts("import RequiresJSFlash from '#{data[:require_js_path]}'")
122
125
  f.puts
123
126
  f.puts("<RequiresJSFlash />")
124
127
  end
@@ -183,8 +186,8 @@ namespace :docs do
183
186
  end
184
187
 
185
188
  component_args = {
186
- "component" => short_name,
187
- "source" => "https://github.com/primer/view_components/tree/main/app/components/primer/#{component.to_s.demodulize.underscore}.rb",
189
+ "component" => data[:title],
190
+ "source" => data[:source],
188
191
  "parameters" => args
189
192
  }
190
193
 
@@ -225,23 +228,12 @@ namespace :docs do
225
228
  f.puts("## Examples")
226
229
 
227
230
  initialize_method.tags(:example).each do |tag|
228
- name = tag.name
229
- description = nil
230
- code = nil
231
-
232
- if tag.text.include?("@description")
233
- splitted = tag.text.split(/@description|@code/)
234
- description = splitted.second.gsub(/^[ \t]{2}/, "").strip
235
- code = splitted.last.gsub(/^[ \t]{2}/, "").strip
236
- else
237
- code = tag.text
238
- end
239
-
231
+ name, description, code = parse_example_tag(tag)
240
232
  f.puts
241
233
  f.puts("### #{name}")
242
234
  if description
243
235
  f.puts
244
- f.puts(description)
236
+ f.puts(view_context.render(inline: description.squish))
245
237
  end
246
238
  f.puts
247
239
  html = view_context.render(inline: code)
@@ -328,13 +320,14 @@ namespace :docs do
328
320
  f.puts(" class #{short_name}Preview < ViewComponent::Preview")
329
321
 
330
322
  yard_example_tags.each_with_index do |tag, index|
331
- method_name = tag.name.split("|").first.downcase.parameterize.underscore
323
+ name, _, code = parse_example_tag(tag)
324
+ method_name = name.split("|").first.downcase.parameterize.underscore
332
325
  f.puts(" def #{method_name}; end")
333
326
  f.puts unless index == yard_example_tags.size - 1
334
327
  path = Pathname.new("demo/test/components/previews/primer/docs/#{short_name.underscore}_preview/#{method_name}.html.erb")
335
328
  path.dirname.mkdir unless path.dirname.exist?
336
329
  File.open(path, "w") do |view_file|
337
- view_file.puts(tag.text.to_s)
330
+ view_file.puts(code.to_s)
338
331
  end
339
332
  end
340
333
 
@@ -346,6 +339,7 @@ namespace :docs do
346
339
  end
347
340
 
348
341
  def generate_yard_registry
342
+ ENV["SKIP_STORYBOOK_PRELOAD"] = "1"
349
343
  require File.expand_path("./../../demo/config/environment.rb", __dir__)
350
344
  require "primer/view_components"
351
345
  require "yard/docs_helper"
@@ -371,6 +365,22 @@ namespace :docs do
371
365
  registry
372
366
  end
373
367
 
368
+ def parse_example_tag(tag)
369
+ name = tag.name
370
+ description = nil
371
+ code = nil
372
+
373
+ if tag.text.include?("@description")
374
+ splitted = tag.text.split(/@description|@code/)
375
+ description = splitted.second.gsub(/^[ \t]{2}/, "").strip
376
+ code = splitted.last.gsub(/^[ \t]{2}/, "").strip
377
+ else
378
+ code = tag.text
379
+ end
380
+
381
+ [name, description, code]
382
+ end
383
+
374
384
  def pretty_default_value(tag, component)
375
385
  params = tag.object.parameters.find { |param| [tag.name.to_s, tag.name.to_s + ":"].include?(param[0]) }
376
386
  default = tag.defaults&.first || params&.second
@@ -384,4 +394,49 @@ namespace :docs do
384
394
 
385
395
  pretty_value(constant_value)
386
396
  end
397
+
398
+ def docs_metadata(component)
399
+ (status_module, short_name) = status_module_and_short_name(component)
400
+ status_path = status_module.nil? ? "" : "#{status_module}/"
401
+ status = component.status.to_s
402
+
403
+ {
404
+ title: short_name,
405
+ component_id: short_name.underscore,
406
+ status: status.capitalize,
407
+ source: source_url(component),
408
+ storybook: storybook_url(component),
409
+ path: "docs/content/components/#{status_path}#{short_name.downcase}.md",
410
+ example_path: example_path(component),
411
+ require_js_path: require_js_path(component)
412
+ }
413
+ end
414
+
415
+ def source_url(component)
416
+ path = component.name.split("::").map(&:underscore).join("/")
417
+
418
+ "https://github.com/primer/view_components/tree/main/app/components/#{path}.rb"
419
+ end
420
+
421
+ def storybook_url(component)
422
+ path = component.name.split("::").map { |n| n.underscore.dasherize }.join("-")
423
+
424
+ "https://primer.style/view-components/stories/?path=/story/#{path}"
425
+ end
426
+
427
+ def example_path(component)
428
+ example_path = "../../src/@primer/gatsby-theme-doctocat/components/example"
429
+ example_path = "../#{example_path}" if status_module?(component)
430
+ example_path
431
+ end
432
+
433
+ def require_js_path(component)
434
+ require_js_path = "../../src/@primer/gatsby-theme-doctocat/components/requires-js-flash"
435
+ require_js_path = "../#{require_js_path}" if status_module?(component)
436
+ require_js_path
437
+ end
438
+
439
+ def status_module?(component)
440
+ (%w[Alpha Beta] & component.name.split("::")).any?
441
+ end
387
442
  end
@@ -5,29 +5,23 @@ 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
10
11
  SUPPORTED_KEYS = %i[
11
12
  anim
12
13
  d
13
14
  float
15
+ height
14
16
  hide
15
17
  m mt mr mb ml mx my
16
18
  p pt pr pb pl px py
17
19
  position
18
20
  wb
21
+ width
19
22
  v
20
23
  ].freeze
21
24
 
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
25
  BREAKPOINTS = [nil, "sm", "md", "lg", "xl"].freeze
32
26
 
33
27
  css_data =
@@ -52,7 +46,7 @@ namespace :utilities do
52
46
  key = ""
53
47
 
54
48
  # Look for a replacement key
55
- REPLACEMENT_KEYS.each do |k, v|
49
+ Primer::Classify::Utilities::REPLACEMENT_KEYS.each do |k, v|
56
50
  next unless classname.match?(Regexp.new(k))
57
51
 
58
52
  key = v
@@ -29,7 +29,7 @@ module YARD
29
29
  end
30
30
 
31
31
  def link_to_accessibility
32
- "[Accessibility](#system-arguments)"
32
+ "[Accessibility](#accessibility)"
33
33
  end
34
34
 
35
35
  def link_to_system_arguments_docs
@@ -41,8 +41,10 @@ module YARD
41
41
  end
42
42
 
43
43
  def link_to_component(component)
44
- short_name = component.name.gsub(/Primer|::|Component/, "")
45
- "[#{short_name}](/components/#{short_name.downcase})"
44
+ (status_module, short_name) = status_module_and_short_name(component)
45
+ status_path = status_module.nil? ? "" : "#{status_module}/"
46
+
47
+ "[#{short_name}](/components/#{status_path}#{short_name.downcase})"
46
48
  end
47
49
 
48
50
  def link_to_octicons
@@ -53,6 +55,13 @@ module YARD
53
55
  "[Learn more about best heading practices (WAI Headings)](https://www.w3.org/WAI/tutorials/page-structure/headings/)"
54
56
  end
55
57
 
58
+ def status_module_and_short_name(component)
59
+ name_with_status = component.name.gsub(/Primer::|Component/, "")
60
+
61
+ m = name_with_status.match(/(?<status>Beta|Alpha|Deprecated)?(::)?(?<name>.*)/)
62
+ [m[:status]&.downcase, m[:name].gsub("::", "")]
63
+ end
64
+
56
65
  def pretty_value(val)
57
66
  case val
58
67
  when nil