ariadne_view_components 0.0.1

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