primer_view_components 0.0.46 → 0.0.50

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 (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