ariadne_view_components 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +68 -0
  4. data/app/assets/javascripts/ariadne_view_components.js +2 -0
  5. data/app/assets/javascripts/ariadne_view_components.js.map +1 -0
  6. data/app/assets/stylesheets/application.tailwind.css +3 -0
  7. data/app/components/ariadne/ariadne.ts +14 -0
  8. data/app/components/ariadne/base_button.rb +60 -0
  9. data/app/components/ariadne/base_component.rb +155 -0
  10. data/app/components/ariadne/button_component.html.erb +4 -0
  11. data/app/components/ariadne/button_component.rb +158 -0
  12. data/app/components/ariadne/clipboard_copy_component.html.erb +8 -0
  13. data/app/components/ariadne/clipboard_copy_component.rb +50 -0
  14. data/app/components/ariadne/clipboard_copy_component.ts +19 -0
  15. data/app/components/ariadne/component.rb +123 -0
  16. data/app/components/ariadne/content.rb +12 -0
  17. data/app/components/ariadne/counter_component.rb +100 -0
  18. data/app/components/ariadne/flash_component.html.erb +31 -0
  19. data/app/components/ariadne/flash_component.rb +125 -0
  20. data/app/components/ariadne/heading_component.rb +49 -0
  21. data/app/components/ariadne/heroicon_component.html.erb +7 -0
  22. data/app/components/ariadne/heroicon_component.rb +116 -0
  23. data/app/components/ariadne/image_component.rb +51 -0
  24. data/app/components/ariadne/text.rb +25 -0
  25. data/app/components/ariadne/tooltip_component.rb +105 -0
  26. data/app/lib/ariadne/audited/dsl.rb +32 -0
  27. data/app/lib/ariadne/class_name_helper.rb +22 -0
  28. data/app/lib/ariadne/fetch_or_fallback_helper.rb +100 -0
  29. data/app/lib/ariadne/icon_helper.rb +47 -0
  30. data/app/lib/ariadne/join_style_arguments_helper.rb +14 -0
  31. data/app/lib/ariadne/logger_helper.rb +23 -0
  32. data/app/lib/ariadne/status/dsl.rb +41 -0
  33. data/app/lib/ariadne/tab_nav_helper.rb +35 -0
  34. data/app/lib/ariadne/tabbed_component_helper.rb +39 -0
  35. data/app/lib/ariadne/test_selector_helper.rb +20 -0
  36. data/app/lib/ariadne/underline_nav_helper.rb +44 -0
  37. data/app/lib/ariadne/view_helper.rb +22 -0
  38. data/lib/ariadne/classify/utilities.rb +199 -0
  39. data/lib/ariadne/classify/utilities.yml +1817 -0
  40. data/lib/ariadne/classify/validation.rb +18 -0
  41. data/lib/ariadne/classify.rb +210 -0
  42. data/lib/ariadne/view_components/constants.rb +53 -0
  43. data/lib/ariadne/view_components/engine.rb +30 -0
  44. data/lib/ariadne/view_components/linters.rb +3 -0
  45. data/lib/ariadne/view_components/statuses.rb +14 -0
  46. data/lib/ariadne/view_components/version.rb +7 -0
  47. data/lib/ariadne/view_components.rb +59 -0
  48. data/lib/rubocop/config/default.yml +14 -0
  49. data/lib/rubocop/cop/ariadne/ariadne_heroicon.rb +252 -0
  50. data/lib/rubocop/cop/ariadne/base_cop.rb +26 -0
  51. data/lib/rubocop/cop/ariadne/component_name_migration.rb +35 -0
  52. data/lib/rubocop/cop/ariadne/no_tag_memoize.rb +43 -0
  53. data/lib/rubocop/cop/ariadne/system_argument_instead_of_class.rb +57 -0
  54. data/lib/rubocop/cop/ariadne.rb +3 -0
  55. data/lib/tasks/ariadne_view_components.rake +47 -0
  56. data/lib/tasks/coverage.rake +19 -0
  57. data/lib/tasks/custom_utilities.yml +310 -0
  58. data/lib/tasks/docs.rake +525 -0
  59. data/lib/tasks/helpers/ast_processor.rb +44 -0
  60. data/lib/tasks/helpers/ast_traverser.rb +77 -0
  61. data/lib/tasks/static.rake +15 -0
  62. data/lib/tasks/tailwind.rake +31 -0
  63. data/lib/tasks/utilities.rake +121 -0
  64. data/lib/yard/docs_helper.rb +83 -0
  65. data/lib/yard/renders_many_handler.rb +19 -0
  66. data/lib/yard/renders_one_handler.rb +19 -0
  67. data/static/arguments.yml +251 -0
  68. data/static/assets/view-components.svg +18 -0
  69. data/static/audited_at.json +14 -0
  70. data/static/classes.yml +89 -0
  71. data/static/constants.json +243 -0
  72. data/static/statuses.json +14 -0
  73. data/static/tailwindcss.yml +727 -0
  74. metadata +193 -0
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ariadne::FetchOrFallbackHelper
4
+ # A little helper to enable graceful fallbacks
5
+ #
6
+ # Use this helper to quietly ensure a value is
7
+ # one that you expect:
8
+ #
9
+ # allowed_values - allowed options for *value*
10
+ # given_value - input being coerced
11
+ # fallback - returned if *given_value* is not included in *allowed_values*
12
+ #
13
+ # fetch_or_raise([1,2,3], 5) => 2
14
+ # fetch_or_raise([1,2,3], 1) => 1
15
+ # fetch_or_raise([1,2,3], nil) => 2
16
+ module Ariadne
17
+ # :nodoc:
18
+ module IconHelper
19
+ include FetchOrFallbackHelper
20
+
21
+ def check_icon_presence!(icon, variant)
22
+ return true unless has_partial_icon?(icon, variant)
23
+
24
+ icon_presence!(icon, variant)
25
+ variant_presence!(icon, variant)
26
+ fetch_or_raise(HeroiconsHelper::Icon::VARIANTS, variant)
27
+
28
+ true
29
+ end
30
+
31
+ def has_partial_icon?(icon, variant)
32
+ icon.present? || variant.present?
33
+ end
34
+
35
+ def icon_presence!(icon, variant)
36
+ raise(ArgumentError, "You must provide an `icon` when providing a `variant`.") if icon.blank? && variant.present?
37
+
38
+ true
39
+ end
40
+
41
+ def variant_presence!(icon, variant)
42
+ raise(ArgumentError, "You must provide a `variant` when providing an `icon`.") if icon.present? && variant.blank?
43
+
44
+ true
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ariadne
4
+ # :nodoc:
5
+ module JoinStyleArgumentsHelper
6
+ # Join two `style` arguments
7
+ #
8
+ # join_style_arguments("width: 100%", "height: 100%") =>
9
+ # "width: 100%;height: 100%"
10
+ def join_style_arguments(*args)
11
+ args.compact.join(";")
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ariadne
4
+ # :nodoc:
5
+ module LoggerHelper
6
+ def logger
7
+ return Rails.logger if defined?(Rails) && Rails.logger
8
+
9
+ require "logger"
10
+ Logger.new($stderr)
11
+ end
12
+
13
+ # TODO: test
14
+ def silence_deprecations?
15
+ Rails.application.config.ariadne_view_components.silence_deprecations
16
+ end
17
+
18
+ # TODO: test
19
+ def silence_warnings?
20
+ Rails.application.config.ariadne_view_components.silence_warnings
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Ariadne
6
+ # :nodoc:
7
+ module Status
8
+ # DSL to allow components to register their status.
9
+ #
10
+ # Example:
11
+ #
12
+ # class MyComponent < ViewComponent::Base
13
+ # include Ariadne::Status::Dsl
14
+ # status :experimental
15
+ # end
16
+ module Dsl
17
+ extend ActiveSupport::Concern
18
+
19
+ STATUSES = {
20
+ experimental: :experimental,
21
+ stable: :stable,
22
+ }.freeze
23
+
24
+ class UnknownStatusError < StandardError; end
25
+
26
+ included do
27
+ class_attribute :component_status, instance_writer: false, default: STATUSES[:stable]
28
+ end
29
+
30
+ class_methods do
31
+ def status(status = nil)
32
+ return component_status if status.nil?
33
+
34
+ raise UnknownStatusError, "status #{status} does not exist" if STATUSES[status].nil?
35
+
36
+ self.component_status = STATUSES[status]
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Ariadne
6
+ # Helper to share tab validation logic between components.
7
+ # The component will raise an error if there are 0 or 2+ selected tabs.
8
+ module TabNavHelper
9
+ extend ActiveSupport::Concern
10
+
11
+ EXTRA_ALIGN_DEFAULT = :left
12
+ EXTRA_ALIGN_OPTIONS = [EXTRA_ALIGN_DEFAULT, :right].freeze
13
+
14
+ def tab_nav_tab_classes(classes)
15
+ class_names(
16
+ "tabnav-tab",
17
+ classes
18
+ )
19
+ end
20
+
21
+ def tab_nav_classes(classes)
22
+ class_names(
23
+ "tabnav",
24
+ classes
25
+ )
26
+ end
27
+
28
+ def tab_nav_body_classes(classes)
29
+ class_names(
30
+ "tabnav-tabs",
31
+ classes
32
+ )
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Ariadne
6
+ # Helper to share tab validation logic between components.
7
+ # The component will raise an error if there are 0 or 2+ selected tabs.
8
+ module TabbedComponentHelper
9
+ extend ActiveSupport::Concern
10
+
11
+ class MultipleSelectedTabsError < StandardError; end
12
+
13
+ def before_render
14
+ validate_single_selected_tab unless Rails.env.production?
15
+ end
16
+
17
+ private
18
+
19
+ def aria_label_for_page_nav(label)
20
+ @attributes[:tag] == :nav ? @attributes[:"aria-label"] = label : @body_arguments[:"aria-label"] = label
21
+ end
22
+
23
+ def tab_container_wrapper(with_panel:, **attributes)
24
+ return yield unless with_panel
25
+
26
+ render(Ariadne::TabContainerComponent.new(**attributes)) do
27
+ yield if block_given?
28
+ end
29
+ end
30
+
31
+ def validate_single_selected_tab
32
+ raise MultipleSelectedTabsError, "only one tab can be selected" if selected_tabs_count > 1
33
+ end
34
+
35
+ def selected_tabs_count
36
+ @selected_tabs_count ||= tabs.count(&:selected)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ariadne
4
+ # Module to allow components to deal with the `test_selector` argument.
5
+ # It will only add the selector if env is not Production.
6
+ #
7
+ # test_selector: "foo" => data-test-selector="foo"
8
+ module TestSelectorHelper
9
+ TEST_SELECTOR_TAG = :test_selector
10
+
11
+ def add_test_selector(args)
12
+ if args.key?(TEST_SELECTOR_TAG)
13
+ args[:data] ||= {}
14
+ args[:data][TEST_SELECTOR_TAG] = args[TEST_SELECTOR_TAG]
15
+ end
16
+
17
+ args.except(TEST_SELECTOR_TAG)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Ariadne
6
+ # Helper to share tab validation logic between components.
7
+ # The component will raise an error if there are 0 or 2+ selected tabs.
8
+ module UnderlineNavHelper
9
+ extend ActiveSupport::Concern
10
+
11
+ ALIGN_DEFAULT = :left
12
+ ALIGN_OPTIONS = [ALIGN_DEFAULT, :right].freeze
13
+
14
+ ACTIONS_TAG_DEFAULT = :div
15
+ ACTIONS_TAG_OPTIONS = [ACTIONS_TAG_DEFAULT, :span].freeze
16
+
17
+ def underline_nav_classes(classes, align)
18
+ class_names(
19
+ classes,
20
+ "UnderlineNav",
21
+ "UnderlineNav--right" => align == :right
22
+ )
23
+ end
24
+
25
+ def underline_nav_body_classes(classes)
26
+ class_names(
27
+ "UnderlineNav-body",
28
+ classes,
29
+ "list-style-none"
30
+ )
31
+ end
32
+
33
+ def underline_nav_action_classes(classes)
34
+ class_names("UnderlineNav-actions", classes)
35
+ end
36
+
37
+ def underline_nav_tab_classes(classes)
38
+ class_names(
39
+ "UnderlineNav-item",
40
+ classes
41
+ )
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nocov:
4
+ module Ariadne
5
+ # Module to allow shorthand calls for Ariadne components
6
+ module ViewHelper
7
+ class ViewHelperNotFound < StandardError; end
8
+
9
+ HELPERS = {
10
+ heroicon: "Ariadne::HeroiconComponent",
11
+ heading: "Ariadne::HeadingComponent",
12
+ time_ago: "Ariadne::TimeAgoComponent",
13
+ image: "Ariadne::ImageComponent",
14
+ }.freeze
15
+
16
+ HELPERS.each do |name, component|
17
+ define_method "ariadne_#{name}" do |*args, **kwargs, &block|
18
+ render component.constantize.new(*args, **kwargs), &block
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ # :nodoc:
6
+ module Ariadne
7
+ class Classify
8
+ # Handler for AriadneCSS utility classes loaded from utilities.rake
9
+ class Utilities
10
+ # Load the utilities.yml file.
11
+ # Disabling because we want to load symbols, strings, and integers from the .yml file
12
+ UTILITIES = YAML.load(
13
+ File.read(
14
+ File.join(File.dirname(__FILE__), "./utilities.yml")
15
+ )
16
+ ).freeze
17
+ BREAKPOINTS = ["", "-sm", "-md", "-lg", "-xl"].freeze
18
+
19
+ # Replacements for some classnames that end up being a different argument key
20
+ REPLACEMENT_KEYS = {
21
+ "^anim" => "animation",
22
+ "^v-align" => "vertical_align",
23
+ "^d" => "display",
24
+ "^wb" => "word_break",
25
+ "^v" => "visibility",
26
+ "^width" => "w",
27
+ "^height" => "h",
28
+ "^color-bg" => "bg",
29
+ "^color-border" => "border_color",
30
+ "^color-fg" => "color",
31
+ }.freeze
32
+
33
+ SUPPORTED_KEY_CACHE = Hash.new { |h, k| h[k] = !UTILITIES[k].nil? }
34
+ BREAKPOINT_INDEX_CACHE = Hash.new { |h, k| h[k] = BREAKPOINTS.index(k) }
35
+
36
+ class << self
37
+ attr_accessor :validate_class_names
38
+ alias_method :validate_class_names?, :validate_class_names
39
+
40
+ def classname(key, val, breakpoint = "")
41
+ # For cases when `argument: false` is passed in, treat like we would nil
42
+ return nil unless val
43
+
44
+ if (valid = validate(key, val, breakpoint))
45
+ valid
46
+ else
47
+ # Get selector
48
+ UTILITIES[key][val][BREAKPOINT_INDEX_CACHE[breakpoint]]
49
+ end
50
+ end
51
+
52
+ # Does the Utility class support the given key
53
+ #
54
+ # returns Boolean
55
+ def supported_key?(key)
56
+ SUPPORTED_KEY_CACHE[key]
57
+ end
58
+
59
+ # Does the Utility class support the given key and value
60
+ #
61
+ # returns Boolean
62
+ def supported_value?(key, val)
63
+ supported_key?(key) && !UTILITIES[key][val].nil?
64
+ end
65
+
66
+ # Does the given selector exist in the utilities file
67
+ #
68
+ # returns Boolean
69
+ def supported_selector?(selector)
70
+ # This method is too slow to run in production
71
+ return false unless validate_class_names?
72
+
73
+ find_selector(selector).present?
74
+ end
75
+
76
+ # Is the key and value responsive
77
+ #
78
+ # returns Boolean
79
+ def responsive?(key, val)
80
+ supported_value?(key, val) && UTILITIES[key][val].count > 1
81
+ end
82
+
83
+ # Get the options for the given key
84
+ #
85
+ # returns Array or nil if key not supported
86
+ def mappings(key)
87
+ return unless supported_key?(key)
88
+
89
+ UTILITIES[key].keys
90
+ end
91
+
92
+ # Extract hash from classes ie. "mr-1 mb-2 foo" => { mr: 1, mb: 2, classes: "foo" }
93
+ def classes_to_hash(classes)
94
+ # This method is too slow to run in production
95
+ return { classes: classes } unless validate_class_names?
96
+
97
+ obj = {}
98
+ classes = classes.split
99
+ # Loop through all classes supplied and reject ones we find a match for
100
+ # So when we're at the end of the loop we have classes left with any non-system classes.
101
+ classes.reject! do |classname|
102
+ key, value, index = find_selector(classname)
103
+ next false if key.nil?
104
+
105
+ # Create array if nil
106
+ obj[key] = Array.new(5, nil) if obj[key].nil?
107
+ # Place the arguments in the responsive array based on index mr: [nil, 2]
108
+ obj[key][index] = value
109
+ next true
110
+ end
111
+
112
+ # Transform responsive arrays into arrays without trailing nil, so `mr: [1, nil, nil, nil, nil]` becomes `mr: 1`
113
+ obj.transform_values! do |value|
114
+ value = value.reverse.drop_while(&:nil?).reverse
115
+ if value.count == 1
116
+ value.first
117
+ else
118
+ value
119
+ end
120
+ end
121
+
122
+ # Add back the non-system classes
123
+ obj[:classes] = classes.join(" ") if classes.any?
124
+ obj
125
+ end
126
+
127
+ def classes_to_args(classes)
128
+ hash_to_args(classes_to_hash(classes))
129
+ end
130
+
131
+ def hash_to_args(hash)
132
+ hash.map do |key, value|
133
+ val = case value
134
+ when Symbol
135
+ ":#{value}"
136
+ when String
137
+ value.to_json
138
+ else
139
+ value
140
+ end
141
+
142
+ "#{key}: #{val}"
143
+ end.join(", ")
144
+ end
145
+
146
+ def validate(key, val, breakpoint)
147
+ unless supported_key?(key)
148
+ raise ArgumentError, "#{key} is not a valid Ariadne utility key" if validate_class_names?
149
+
150
+ return ""
151
+ end
152
+
153
+ unless breakpoint.empty? || responsive?(key, val)
154
+ raise ArgumentError, "#{key} does not support responsive values" if validate_class_names?
155
+
156
+ return ""
157
+ end
158
+
159
+ unless supported_value?(key, val)
160
+ raise ArgumentError, "#{val} is not a valid value for :#{key}. Use one of #{mappings(key)}" if validate_class_names?
161
+
162
+ return nil if [true, false].include?(val)
163
+
164
+ return "#{key.to_s.dasherize}-#{val.to_s.dasherize}"
165
+ end
166
+
167
+ nil
168
+ end
169
+
170
+ private def find_selector(selector)
171
+ key = infer_selector_key(selector)
172
+ value_hash = UTILITIES[key]
173
+
174
+ return nil if value_hash.blank?
175
+
176
+ # Each value hash will also contain an array of classnames for breakpoints
177
+ # Key argument `0`, classes `[ "mr-0", "mr-sm-0", "mr-md-0", "mr-lg-0", "mr-xl-0" ]`
178
+ value_hash.each do |key_argument, classnames|
179
+ # Skip each value hash until we get one with the selector
180
+ next unless classnames.include?(selector)
181
+
182
+ # Return [:mr, 0, 1]
183
+ # has index of classname, so we can match it up with responsive array `mr: [nil, 0]`
184
+ return [key, key_argument, classnames.index(selector)]
185
+ end
186
+
187
+ nil
188
+ end
189
+
190
+ private def infer_selector_key(selector)
191
+ REPLACEMENT_KEYS.each do |k, v|
192
+ return v.to_sym if selector.match?(Regexp.new(k))
193
+ end
194
+ selector.split("-").first.to_sym
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end