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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +68 -0
- data/app/assets/javascripts/ariadne_view_components.js +2 -0
- data/app/assets/javascripts/ariadne_view_components.js.map +1 -0
- data/app/assets/stylesheets/application.tailwind.css +3 -0
- data/app/components/ariadne/ariadne.ts +14 -0
- data/app/components/ariadne/base_button.rb +60 -0
- data/app/components/ariadne/base_component.rb +155 -0
- data/app/components/ariadne/button_component.html.erb +4 -0
- data/app/components/ariadne/button_component.rb +158 -0
- data/app/components/ariadne/clipboard_copy_component.html.erb +8 -0
- data/app/components/ariadne/clipboard_copy_component.rb +50 -0
- data/app/components/ariadne/clipboard_copy_component.ts +19 -0
- data/app/components/ariadne/component.rb +123 -0
- data/app/components/ariadne/content.rb +12 -0
- data/app/components/ariadne/counter_component.rb +100 -0
- data/app/components/ariadne/flash_component.html.erb +31 -0
- data/app/components/ariadne/flash_component.rb +125 -0
- data/app/components/ariadne/heading_component.rb +49 -0
- data/app/components/ariadne/heroicon_component.html.erb +7 -0
- data/app/components/ariadne/heroicon_component.rb +116 -0
- data/app/components/ariadne/image_component.rb +51 -0
- data/app/components/ariadne/text.rb +25 -0
- data/app/components/ariadne/tooltip_component.rb +105 -0
- data/app/lib/ariadne/audited/dsl.rb +32 -0
- data/app/lib/ariadne/class_name_helper.rb +22 -0
- data/app/lib/ariadne/fetch_or_fallback_helper.rb +100 -0
- data/app/lib/ariadne/icon_helper.rb +47 -0
- data/app/lib/ariadne/join_style_arguments_helper.rb +14 -0
- data/app/lib/ariadne/logger_helper.rb +23 -0
- data/app/lib/ariadne/status/dsl.rb +41 -0
- data/app/lib/ariadne/tab_nav_helper.rb +35 -0
- data/app/lib/ariadne/tabbed_component_helper.rb +39 -0
- data/app/lib/ariadne/test_selector_helper.rb +20 -0
- data/app/lib/ariadne/underline_nav_helper.rb +44 -0
- data/app/lib/ariadne/view_helper.rb +22 -0
- data/lib/ariadne/classify/utilities.rb +199 -0
- data/lib/ariadne/classify/utilities.yml +1817 -0
- data/lib/ariadne/classify/validation.rb +18 -0
- data/lib/ariadne/classify.rb +210 -0
- data/lib/ariadne/view_components/constants.rb +53 -0
- data/lib/ariadne/view_components/engine.rb +30 -0
- data/lib/ariadne/view_components/linters.rb +3 -0
- data/lib/ariadne/view_components/statuses.rb +14 -0
- data/lib/ariadne/view_components/version.rb +7 -0
- data/lib/ariadne/view_components.rb +59 -0
- data/lib/rubocop/config/default.yml +14 -0
- data/lib/rubocop/cop/ariadne/ariadne_heroicon.rb +252 -0
- data/lib/rubocop/cop/ariadne/base_cop.rb +26 -0
- data/lib/rubocop/cop/ariadne/component_name_migration.rb +35 -0
- data/lib/rubocop/cop/ariadne/no_tag_memoize.rb +43 -0
- data/lib/rubocop/cop/ariadne/system_argument_instead_of_class.rb +57 -0
- data/lib/rubocop/cop/ariadne.rb +3 -0
- data/lib/tasks/ariadne_view_components.rake +47 -0
- data/lib/tasks/coverage.rake +19 -0
- data/lib/tasks/custom_utilities.yml +310 -0
- data/lib/tasks/docs.rake +525 -0
- data/lib/tasks/helpers/ast_processor.rb +44 -0
- data/lib/tasks/helpers/ast_traverser.rb +77 -0
- data/lib/tasks/static.rake +15 -0
- data/lib/tasks/tailwind.rake +31 -0
- data/lib/tasks/utilities.rake +121 -0
- data/lib/yard/docs_helper.rb +83 -0
- data/lib/yard/renders_many_handler.rb +19 -0
- data/lib/yard/renders_one_handler.rb +19 -0
- data/static/arguments.yml +251 -0
- data/static/assets/view-components.svg +18 -0
- data/static/audited_at.json +14 -0
- data/static/classes.yml +89 -0
- data/static/constants.json +243 -0
- data/static/statuses.json +14 -0
- data/static/tailwindcss.yml +727 -0
- metadata +193 -0
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ariadne/classify"
|
4
|
+
|
5
|
+
module Ariadne
|
6
|
+
# All Ariadne ViewComponents accept a standard set of options: classes, which match Tailwind CSS classes, and attributes, which match HTML attributes..
|
7
|
+
#
|
8
|
+
# Under the hood, classes are [mapped](https://github.com/ariadne/view_components/blob/main/lib/ariadne/classify.rb) to Tailwind CSS classes, with any unconsumed attributes passed to Rails' [`content_tag`](https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag).
|
9
|
+
class BaseComponent < Ariadne::Component
|
10
|
+
SELF_CLOSING_TAGS = [:area, :base, :br, :col, :embed, :hr, :img, :input, :link, :meta, :param, :source, :track, :wbr].freeze
|
11
|
+
|
12
|
+
# ## HTML attributes
|
13
|
+
#
|
14
|
+
# System arguments include most HTML attributes. For example:
|
15
|
+
#
|
16
|
+
# | Name | Type | Description |
|
17
|
+
# | :- | :- | :- |
|
18
|
+
# | `aria` | `Hash` | Aria attributes: `aria: { label: "foo" }` renders `aria-label='foo'`. |
|
19
|
+
# | `data` | `Hash` | Data attributes: `data: { foo: :bar }` renders `data-foo='bar'`. |
|
20
|
+
# | `height` | `Integer` | The height. |
|
21
|
+
# | `hidden` | `Boolean` | Whether to assign the `hidden` attribute. |
|
22
|
+
# | `style` | `String` | Inline styles. |
|
23
|
+
# | `title` | `String` | The `title` attribute. |
|
24
|
+
# | `width` | `Integer` | The width. |
|
25
|
+
#
|
26
|
+
# ## Animation
|
27
|
+
#
|
28
|
+
# | Name | Type | Description |
|
29
|
+
# | :- | :- | :- |
|
30
|
+
# | `animation` | Symbol | <%= one_of(Ariadne::Classify::Utilities.mappings(:animation)) %> |
|
31
|
+
#
|
32
|
+
# ## Border
|
33
|
+
#
|
34
|
+
# | Name | Type | Description |
|
35
|
+
# | :- | :- | :- |
|
36
|
+
# | `border_bottom` | Integer | Set to `0` to remove the bottom border. |
|
37
|
+
# | `border_left` | Integer | Set to `0` to remove the left border. |
|
38
|
+
# | `border_radius` | Integer | <%= one_of([0, 1, 2, 3]) %> |
|
39
|
+
# | `border_right` | Integer | Set to `0` to remove the right border. |
|
40
|
+
# | `border_top` | Integer | Set to `0` to remove the top border. |
|
41
|
+
# | `border` | Symbol | <%= one_of([:left, :top, :bottom, :right, :y, :x, true]) %> |
|
42
|
+
# | `box_shadow` | Boolean, Symbol | Box shadow. <%= one_of([true, :medium, :large, :extra_large, :none]) %> |
|
43
|
+
#
|
44
|
+
# ## Color
|
45
|
+
#
|
46
|
+
# | Name | Type | Description |
|
47
|
+
# | :- | :- | :- |
|
48
|
+
# | `bg` | Symbol | Background color. <%= one_of(Ariadne::Classify::Utilities.mappings(:bg)) %> |
|
49
|
+
# | `border_color` | Symbol | Border color. <%= one_of(Ariadne::Classify::Utilities.mappings(:border_color)) %> |
|
50
|
+
# | `color` | Symbol | Text color. <%= one_of(Ariadne::Classify::Utilities.mappings(:color)) %> |
|
51
|
+
#
|
52
|
+
# ## Flex
|
53
|
+
#
|
54
|
+
# | Name | Type | Description |
|
55
|
+
# | :- | :- | :- |
|
56
|
+
# | `align_items` | Symbol | <%= one_of(Ariadne::Classify::FLEX_ALIGN_ITEMS_VALUES) %> |
|
57
|
+
# | `align_self` | Symbol | <%= one_of(Ariadne::Classify::FLEX_ALIGN_SELF_VALUES) %> |
|
58
|
+
# | `direction` | Symbol | <%= one_of(Ariadne::Classify::FLEX_DIRECTION_VALUES) %> |
|
59
|
+
# | `flex` | Integer, Symbol | <%= one_of(Ariadne::Classify::FLEX_VALUES) %> |
|
60
|
+
# | `flex_grow` | Integer | To enable, set to `0`. |
|
61
|
+
# | `flex_shrink` | Integer | To enable, set to `0`. |
|
62
|
+
# | `flex_wrap` | Symbol | <%= one_of(Ariadne::Classify::FLEX_WRAP_MAPPINGS.keys) %> |
|
63
|
+
# | `justify_content` | Symbol | <%= one_of(Ariadne::Classify::FLEX_JUSTIFY_CONTENT_VALUES) %> |
|
64
|
+
#
|
65
|
+
# ## Grid
|
66
|
+
#
|
67
|
+
# | Name | Type | Description |
|
68
|
+
# | :- | :- | :- |
|
69
|
+
# | `clearfix` | Boolean | Whether to assign the `clearfix` class. |
|
70
|
+
# | `col` | Integer | Number of columns. <%= one_of(Ariadne::Classify::Utilities.mappings(:col)) %> |
|
71
|
+
# | `container` | Symbol | Size of the container. <%= one_of(Ariadne::Classify::Utilities.mappings(:container)) %> |
|
72
|
+
#
|
73
|
+
# ## Layout
|
74
|
+
#
|
75
|
+
# | Name | Type | Description |
|
76
|
+
# | :- | :- | :- |
|
77
|
+
# | `display` | Symbol | <%= one_of(Ariadne::Classify::Utilities.mappings(:display)) %> |
|
78
|
+
# | `w` | Symbol | <%= one_of(Ariadne::Classify::Utilities.mappings(:w)) %> |
|
79
|
+
# | `h` | Symbol | <%= one_of(Ariadne::Classify::Utilities.mappings(:h)) %> |
|
80
|
+
# | `hide` | Symbol | Hide the element at a specific breakpoint. <%= one_of(Ariadne::Classify::Utilities.mappings(:hide)) %> |
|
81
|
+
# | `visibility` | Symbol | Visibility. <%= one_of(Ariadne::Classify::Utilities.mappings(:visibility)) %> |
|
82
|
+
# | `vertical_align` | Symbol | <%= one_of(Ariadne::Classify::Utilities.mappings(:vertical_align)) %> |
|
83
|
+
#
|
84
|
+
# ## Position
|
85
|
+
#
|
86
|
+
# | Name | Type | Description |
|
87
|
+
# | :- | :- | :- |
|
88
|
+
# | `bottom` | Boolean | If `false`, sets `bottom: 0`. |
|
89
|
+
# | `float` | Symbol | <%= one_of(Ariadne::Classify::Utilities.mappings(:float)) %> |
|
90
|
+
# | `left` | Boolean | If `false`, sets `left: 0`. |
|
91
|
+
# | `position` | Symbol | <%= one_of(Ariadne::Classify::Utilities.mappings(:position)) %> |
|
92
|
+
# | `right` | Boolean | If `false`, sets `right: 0`. |
|
93
|
+
# | `top` | Boolean | If `false`, sets `top: 0`. |
|
94
|
+
#
|
95
|
+
# ## Spacing
|
96
|
+
#
|
97
|
+
# | Name | Type | Description |
|
98
|
+
# | :- | :- | :- |
|
99
|
+
# | `m` | Integer | Margin. <%= one_of(Ariadne::Classify::Utilities.mappings(:m)) %> |
|
100
|
+
# | `mb` | Integer | Margin bottom. <%= one_of(Ariadne::Classify::Utilities.mappings(:mb)) %> |
|
101
|
+
# | `ml` | Integer | Margin left. <%= one_of(Ariadne::Classify::Utilities.mappings(:ml)) %> |
|
102
|
+
# | `mr` | Integer | Margin right. <%= one_of(Ariadne::Classify::Utilities.mappings(:mr)) %> |
|
103
|
+
# | `mt` | Integer | Margin top. <%= one_of(Ariadne::Classify::Utilities.mappings(:mt)) %> |
|
104
|
+
# | `mx` | Integer | Horizontal margins. <%= one_of(Ariadne::Classify::Utilities.mappings(:mx)) %> |
|
105
|
+
# | `my` | Integer | Vertical margins. <%= one_of(Ariadne::Classify::Utilities.mappings(:my)) %> |
|
106
|
+
# | `p` | Integer | Padding. <%= one_of(Ariadne::Classify::Utilities.mappings(:p)) %> |
|
107
|
+
# | `pb` | Integer | Padding bottom. <%= one_of(Ariadne::Classify::Utilities.mappings(:pb)) %> |
|
108
|
+
# | `pl` | Integer | Padding left. <%= one_of(Ariadne::Classify::Utilities.mappings(:pl)) %> |
|
109
|
+
# | `pr` | Integer | Padding right. <%= one_of(Ariadne::Classify::Utilities.mappings(:pr)) %> |
|
110
|
+
# | `pt` | Integer | Padding left. <%= one_of(Ariadne::Classify::Utilities.mappings(:pt)) %> |
|
111
|
+
# | `px` | Integer | Horizontal padding. <%= one_of(Ariadne::Classify::Utilities.mappings(:px)) %> |
|
112
|
+
# | `py` | Integer | Vertical padding. <%= one_of(Ariadne::Classify::Utilities.mappings(:py)) %> |
|
113
|
+
#
|
114
|
+
# ## Typography
|
115
|
+
#
|
116
|
+
# | Name | Type | Description |
|
117
|
+
# | :- | :- | :- |
|
118
|
+
# | `font_family` | Symbol | Font weight. <%= one_of([:mono]) %> |
|
119
|
+
# | `font_size` | String, Integer, Symbol | <%= one_of(["00", 0, 1, 2, 3, 4, 5, 6, :small, :normal]) %> |
|
120
|
+
# | `font_style` | Symbol | Font weight. <%= one_of([:italic]) %> |
|
121
|
+
# | `font_weight` | Symbol | Font weight. <%= one_of([:light, :normal, :bold, :emphasized]) %> |
|
122
|
+
# | `text_align` | Symbol | Text alignment. <%= one_of([:left, :right, :center]) %> |
|
123
|
+
# | `text_transform` | Symbol | Text alignment. <%= one_of([:uppercase]) %> |
|
124
|
+
# | `underline` | Boolean | Whether text should be underlined. |
|
125
|
+
# | `word_break` | Symbol | Whether to break words on line breaks. <%= one_of(Ariadne::Classify::Utilities.mappings(:word_break)) %> |
|
126
|
+
#
|
127
|
+
# ## Other
|
128
|
+
#
|
129
|
+
# | Name | Type | Description |
|
130
|
+
# | :- | :- | :- |
|
131
|
+
# | classes | String | CSS class name value to be concatenated with generated Tailwind CSS classes. |
|
132
|
+
# | test_selector | String | Adds `data-test-selector='given value'` in non-Production environments for testing purposes. |
|
133
|
+
def initialize(tag:, classes: "", attributes: {})
|
134
|
+
@tag = tag
|
135
|
+
|
136
|
+
@attributes = validate_attributes(tag: tag, attributes: attributes)
|
137
|
+
@attributes[:"data-ariadne-view-component"] = true
|
138
|
+
|
139
|
+
classes = classes.present? ? @attributes.merge(classes: classes) : @attributes.fetch(:classes, "")
|
140
|
+
@classes = Ariadne::Classify.call(classes)
|
141
|
+
|
142
|
+
# Filter out Ariadne keys so they don't get assigned as HTML attributes
|
143
|
+
@content_tag_args = add_test_selector(@attributes).except(*Ariadne::Classify::Utilities::UTILITIES.keys)
|
144
|
+
end
|
145
|
+
|
146
|
+
def call
|
147
|
+
options = @content_tag_args.merge(@classes)
|
148
|
+
if SELF_CLOSING_TAGS.include?(@tag)
|
149
|
+
tag(@tag, options)
|
150
|
+
else
|
151
|
+
content_tag(@tag, content, options)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ariadne
|
4
|
+
# Use `Button` for actions (e.g. in forms). Use links for destinations, or moving from one page to another.
|
5
|
+
class ButtonComponent < Ariadne::Component
|
6
|
+
include IconHelper
|
7
|
+
|
8
|
+
DEFAULT_SCHEME = :default
|
9
|
+
LINK_SCHEME = :link
|
10
|
+
|
11
|
+
SCHEME_CLASS_MAPPINGS = {
|
12
|
+
default: "text-blue-800 bg-blue-50 hover:bg-blue-100 border-blue-300 focus:ring-offset-blue-50 focus:ring-blue-600",
|
13
|
+
info: "text-blue-800 bg-blue-50 hover:bg-blue-100 border-blue-300 focus:ring-offset-blue-50 focus:ring-blue-600",
|
14
|
+
success: "text-green-800 bg-green-50 hover:bg-green-100 border-green-300 focus:ring-offset-green-50 focus:ring-green-600",
|
15
|
+
warning: "text-yellow-800 bg-yellow-50 hover:bg-yellow-100 border-yellow-300 focus:ring-offset-yellow-50 focus:ring-yellow-600",
|
16
|
+
danger: "text-red-800 bg-red-50 hover:bg-red-100 border-red-300 focus:ring-offset-red-50 focus:ring-red-600",
|
17
|
+
}.freeze
|
18
|
+
VALID_SCHEMES = SCHEME_CLASS_MAPPINGS.keys.freeze
|
19
|
+
|
20
|
+
# Leading visuals appear to the left of the button text.
|
21
|
+
#
|
22
|
+
# Use:
|
23
|
+
#
|
24
|
+
# - `icon` for a <%= link_to_component(Ariadne::HeroiconComponent) %>.
|
25
|
+
#
|
26
|
+
# @param tag [Symbol, String] The rendered tag name
|
27
|
+
# @param classes [String] <%= link_to_classes_docs %>
|
28
|
+
# @param icon [String] Name of <%= link_to_heroicons %> to use.
|
29
|
+
# @param variant [String] <%= one_of(HeroiconsHelper::Icon::VARIANTS, sort: false) %>
|
30
|
+
# @param attributes [Hash] Same arguments as <%= link_to_component(Ariadne::HeroiconComponent) %>.
|
31
|
+
renders_one :icon, lambda { |tag: :svg, icon:, variant:, classes: "", attributes: {}|
|
32
|
+
@icon = icon
|
33
|
+
@variant = variant
|
34
|
+
tag = check_incoming_tag(:svg, tag)
|
35
|
+
Ariadne::HeroiconComponent.new(tag: tag, icon: icon, variant: variant, classes: classes, attributes: attributes)
|
36
|
+
}
|
37
|
+
|
38
|
+
# Trailing visuals appear to the right of the button text.
|
39
|
+
#
|
40
|
+
# Use:
|
41
|
+
#
|
42
|
+
# - `counter` for a <%= link_to_component(Ariadne::CounterComponent) %>.
|
43
|
+
#
|
44
|
+
# @param tag [Symbol, String] The rendered tag name
|
45
|
+
# @param classes [String] <%= link_to_classes_docs %>
|
46
|
+
# @param counter [Number] The starting counter value
|
47
|
+
# @param attributes [Hash] Same arguments as <%= link_to_component(Ariadne::CounterComponent) %>.
|
48
|
+
renders_one :counter, lambda { |tag: :span, count: 0, classes: "", attributes: {}|
|
49
|
+
tag = check_incoming_tag(:span, tag)
|
50
|
+
attributes[:ml] = check_incoming_value(2, ml: attributes[:ml])
|
51
|
+
|
52
|
+
Ariadne::CounterComponent.new(tag: tag, count: count, classes: classes, attributes: attributes)
|
53
|
+
}
|
54
|
+
|
55
|
+
# `Tooltip` that appears on mouse hover or keyboard focus over the button. Use tooltips sparingly and as a last resort.
|
56
|
+
# **Important:** This tooltip defaults to `type: :description`. In a few scenarios, `type: :label` may be more appropriate.
|
57
|
+
# Consult the <%= link_to_component(Ariadne::TooltipComponent) %> documentation for more information.
|
58
|
+
#
|
59
|
+
# @param tag [Symbol, String] The rendered tag name
|
60
|
+
# @param text [String] The text content of the tooltip. This should be brief and no longer than a sentence.
|
61
|
+
# @param direction [Symbol] <%= one_of(Ariadne::TooltipComponent::DIRECTION_OPTIONS) %>
|
62
|
+
# @param classes [String] <%= link_to_classes_docs %>
|
63
|
+
# @param attributes [Hash] Same arguments as <%= link_to_component(Ariadne::TooltipComponent) %>.
|
64
|
+
renders_one :tooltip, lambda { |tag: :"tool-tip", text:, direction: Ariadne::TooltipComponent::DIRECTION_DEFAULT, type: Ariadne::TooltipComponent::TYPE_DEFAULT, classes: "", attributes: {}|
|
65
|
+
raise ArgumentError, "Buttons with a tooltip must have a unique `id` set on the `Button`." if @id.blank?
|
66
|
+
|
67
|
+
# TODO: test this
|
68
|
+
tag = check_incoming_tag(:"tool-tip", tag)
|
69
|
+
attributes[:for] = @id
|
70
|
+
attributes[:type] = check_incoming_attribute(:description, attributes[:type])
|
71
|
+
|
72
|
+
Ariadne::TooltipComponent.new(tag: tag, text: text, direction: direction, type: type, classes: classes, attributes: attributes)
|
73
|
+
}
|
74
|
+
|
75
|
+
# @example Schemes
|
76
|
+
# <%= render(Ariadne::ButtonComponent.new) { "Default" } %>
|
77
|
+
# <%= render(Ariadne::ButtonComponent.new(scheme: :default)) { "Default" } %>
|
78
|
+
# <%= render(Ariadne::ButtonComponent.new(scheme: :info)) { "Info" } %>
|
79
|
+
# <%= render(Ariadne::ButtonComponent.new(scheme: :success)) { "Success" } %>
|
80
|
+
# <%= render(Ariadne::ButtonComponent.new(scheme: :warning)) { "Warning" } %>
|
81
|
+
# <%= render(Ariadne::ButtonComponent.new(scheme: :danger)) { "Danger" } %>
|
82
|
+
#
|
83
|
+
# @example Sizes
|
84
|
+
# <%= render(Ariadne::ButtonComponent.new(size: :s)) { "Small" } %>
|
85
|
+
# <%= render(Ariadne::ButtonComponent.new(size: :m)) { "Medium" } %>
|
86
|
+
#
|
87
|
+
# @example With leading visual
|
88
|
+
# <%= render(Ariadne::ButtonComponent.new) do |c| %>
|
89
|
+
# <% c.icon(icon: :star, variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, classes: "text-yellow-600") %>
|
90
|
+
# Button
|
91
|
+
# <% end %>
|
92
|
+
#
|
93
|
+
# @example With trailing visual
|
94
|
+
# <%= render(Ariadne::ButtonComponent.new) do |c| %>
|
95
|
+
# <% c.counter(count: 15) %>
|
96
|
+
# Button
|
97
|
+
# <% end %>
|
98
|
+
#
|
99
|
+
# @example With leading and trailing visuals
|
100
|
+
# <%= render(Ariadne::ButtonComponent.new) do |c| %>
|
101
|
+
# <% c.icon(icon: :star, variant: HeroiconsHelper::Icon::VARIANT_OUTLINE) %>
|
102
|
+
# <% c.counter(count: 15) %>
|
103
|
+
# Button
|
104
|
+
# <% end %>
|
105
|
+
#
|
106
|
+
# @example With tooltip
|
107
|
+
# @description
|
108
|
+
# Use tooltips sparingly and as a last resort. Consult the <%= link_to_component(Ariadne::TooltipComponent) %> documentation for more information.
|
109
|
+
# @code
|
110
|
+
# <%= render(Ariadne::ButtonComponent.new(attributes: { id: "button-with-tooltip" })) do |c| %>
|
111
|
+
# <% c.tooltip(text: "Tooltip text") %>
|
112
|
+
# Button
|
113
|
+
# <% end %>
|
114
|
+
#
|
115
|
+
# @param tag [Symbol] <%= one_of(Ariadne::BaseButton::TAG_OPTIONS) %>
|
116
|
+
# @param scheme [Symbol] <%= one_of(Ariadne::ButtonComponent::VALID_SCHEMES) %>
|
117
|
+
# @param size [Symbol] <%= one_of(Ariadne::BaseButton::VALID_SIZES) %>
|
118
|
+
# @param type [Symbol] <%= one_of(Ariadne::BaseButton::TYPE_OPTIONS) %>
|
119
|
+
# @param classes [String] <%= link_to_classes_docs %>
|
120
|
+
# @param attributes [Hash] <%= link_to_attributes_docs %>
|
121
|
+
def initialize(
|
122
|
+
tag: Ariadne::BaseButton::DEFAULT_TAG,
|
123
|
+
scheme: DEFAULT_SCHEME,
|
124
|
+
size: BaseButton::DEFAULT_SIZE,
|
125
|
+
classes: "",
|
126
|
+
attributes: {}
|
127
|
+
)
|
128
|
+
@tag = tag
|
129
|
+
@scheme = scheme
|
130
|
+
|
131
|
+
@attributes = attributes
|
132
|
+
@id = @attributes[:id]
|
133
|
+
|
134
|
+
@size = fetch_or_raise(Ariadne::BaseButton::VALID_SIZES, size)
|
135
|
+
@scheme = fetch_or_raise(VALID_SCHEMES, scheme)
|
136
|
+
|
137
|
+
@classes = class_names(
|
138
|
+
SCHEME_CLASS_MAPPINGS[@scheme],
|
139
|
+
classes
|
140
|
+
)
|
141
|
+
end
|
142
|
+
|
143
|
+
private def trimmed_content
|
144
|
+
return if content.blank?
|
145
|
+
|
146
|
+
trimmed_content = content.strip
|
147
|
+
|
148
|
+
return trimmed_content unless content.html_safe?
|
149
|
+
|
150
|
+
# strip unsets `html_safe`, so we have to set it back again to guarantee that HTML blocks won't break
|
151
|
+
trimmed_content.html_safe # rubocop:disable Rails/OutputSafety
|
152
|
+
end
|
153
|
+
|
154
|
+
private def link?
|
155
|
+
@scheme == LINK_SCHEME
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<%= render Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes.merge(:"data-controller" => DATA_CONTROLLER, :"data-action" => DATA_ACTION)) do %>
|
2
|
+
<% if content.present? %>
|
3
|
+
<%= content %>
|
4
|
+
<% else %>
|
5
|
+
<%= render Ariadne::HeroiconComponent.new(icon: :duplicate, variant: HeroiconsHelper::Icon::VARIANT_OUTLINE) %>
|
6
|
+
<%= render Ariadne::HeroiconComponent.new(icon: :check, variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, attributes: { color: :success, style: "display: none;" }) %>
|
7
|
+
<% end %>
|
8
|
+
<% end %>
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ariadne
|
4
|
+
# Use `ClipboardCopyComponent` to copy element text content or input values to the clipboard.
|
5
|
+
#
|
6
|
+
# @accessibility
|
7
|
+
# Always set an accessible label to help the user interact with the component.
|
8
|
+
class ClipboardCopyComponent < Ariadne::Component
|
9
|
+
DEFAULT_TAG = :"clipboard-copy"
|
10
|
+
|
11
|
+
DATA_CONTROLLER = "clipboard_copy_component"
|
12
|
+
DATA_ACTION = "click->clipboard_copy_component#copy"
|
13
|
+
|
14
|
+
# @example Default
|
15
|
+
# <%= render(Ariadne::ClipboardCopyComponent.new(value: "Text to copy", aria_label: "Copy text to the system clipboard" )) %>
|
16
|
+
#
|
17
|
+
# @example With text instead of icons
|
18
|
+
# <%= render(Ariadne::ClipboardCopyComponent.new(value: "Text to copy", aria_label: "Copy text to the system clipboard" )) do %>
|
19
|
+
# Click to copy!
|
20
|
+
# <% end %>
|
21
|
+
#
|
22
|
+
# @example Copying from an element
|
23
|
+
# <%= render(Ariadne::ClipboardCopyComponent.new(attributes: { for: "blob-path"}, aria_label: "Copy text to the system clipboard" )) %>
|
24
|
+
# <div id="blob-path">src/index.js</div>
|
25
|
+
#
|
26
|
+
# @param tag [Symbol, String] The rendered tag name
|
27
|
+
# @param classes [String] <%= link_to_classes_docs %>
|
28
|
+
# @param value [String] Text to copy into the users clipboard when they click the component.
|
29
|
+
# @param aria_label [String] Text for accessibility. Can also be passed in as part of `attributes`, but it must be present.
|
30
|
+
# @param attributes [Hash] <%= link_to_attributes_docs %>
|
31
|
+
def initialize(tag: DEFAULT_TAG, value: "", aria_label: "", classes: "", attributes: {})
|
32
|
+
@attributes = attributes
|
33
|
+
@value = value
|
34
|
+
|
35
|
+
@attributes[:"aria-label"] = aria_label
|
36
|
+
|
37
|
+
validate!
|
38
|
+
|
39
|
+
@classes = classes
|
40
|
+
@tag = check_incoming_tag(DEFAULT_TAG, tag)
|
41
|
+
@attributes[:value] = value if value.present?
|
42
|
+
end
|
43
|
+
|
44
|
+
private def validate!
|
45
|
+
validate_aria_label!
|
46
|
+
raise ArgumentError, "Must provide either `value` or `for`" if @value.blank? && @attributes[:for].blank?
|
47
|
+
raise ArgumentError, "Must provide only `value` or `for`, not both" if @value.present? && @attributes[:for].present?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import {Controller} from '@hotwired/stimulus'
|
2
|
+
|
3
|
+
export default class ClipboardCopyComponent extends Controller {
|
4
|
+
copy() {
|
5
|
+
const value = this.element.attributes.getNamedItem('value')
|
6
|
+
|
7
|
+
const forNode = this.element.attributes.getNamedItem('for')
|
8
|
+
|
9
|
+
if (value) {
|
10
|
+
navigator.clipboard.writeText(value.value)
|
11
|
+
} else if (forNode) {
|
12
|
+
const node = document.getElementById(forNode.value)
|
13
|
+
navigator.clipboard.writeText(node?.textContent || '')
|
14
|
+
} else {
|
15
|
+
// just copy inner text
|
16
|
+
navigator.clipboard.writeText(this.element.textContent || '')
|
17
|
+
}
|
18
|
+
}
|
19
|
+
}
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "view_component/version"
|
4
|
+
|
5
|
+
require "heroicons_helper"
|
6
|
+
|
7
|
+
module Ariadne
|
8
|
+
# @private
|
9
|
+
class Component < ViewComponent::Base
|
10
|
+
include ViewComponent::SlotableV2 unless ViewComponent::Base < ViewComponent::SlotableV2
|
11
|
+
include ViewComponent::PolymorphicSlots
|
12
|
+
include ClassNameHelper
|
13
|
+
include FetchOrFallbackHelper
|
14
|
+
include TestSelectorHelper
|
15
|
+
include JoinStyleArgumentsHelper
|
16
|
+
include ViewHelper
|
17
|
+
include Status::Dsl
|
18
|
+
include Audited::Dsl
|
19
|
+
include LoggerHelper
|
20
|
+
|
21
|
+
INVALID_ARIA_LABEL_TAGS = [:div, :span, :p].freeze
|
22
|
+
|
23
|
+
private def raise_on_invalid_options?
|
24
|
+
Rails.application.config.ariadne_view_components.raise_on_invalid_options
|
25
|
+
end
|
26
|
+
|
27
|
+
private def raise_on_invalid_aria?
|
28
|
+
Rails.application.config.ariadne_view_components.raise_on_invalid_aria
|
29
|
+
end
|
30
|
+
|
31
|
+
private def deprecated_component_warning(new_class: nil, version: nil)
|
32
|
+
return if silence_deprecations?
|
33
|
+
|
34
|
+
message = "#{self.class.name} is deprecated"
|
35
|
+
message += " and will be removed in v#{version}." if version
|
36
|
+
message += " Use #{new_class.name} instead." if new_class
|
37
|
+
|
38
|
+
ActiveSupport::Deprecation.warn(message)
|
39
|
+
end
|
40
|
+
|
41
|
+
private def aria(val, attributes)
|
42
|
+
attributes[:"aria-#{val}"] || attributes.dig(:aria, val.to_sym)
|
43
|
+
end
|
44
|
+
|
45
|
+
private def validate_aria_label!
|
46
|
+
aria_label = aria("label", @attributes)
|
47
|
+
raise ArgumentError, "`aria-label` is required." if aria_label.blank?
|
48
|
+
end
|
49
|
+
|
50
|
+
private def check_denylist(denylist = [], attributes = {})
|
51
|
+
if should_raise_error?
|
52
|
+
|
53
|
+
# Convert denylist from:
|
54
|
+
# { [:p, :pt] => "message" } to:
|
55
|
+
# { p: "message", pt: "message" }
|
56
|
+
unpacked_denylist =
|
57
|
+
denylist.each_with_object({}) do |(keys, value), memo|
|
58
|
+
keys.each { |key| memo[key] = value }
|
59
|
+
end
|
60
|
+
|
61
|
+
violations = unpacked_denylist.keys & attributes.keys
|
62
|
+
|
63
|
+
if violations.any?
|
64
|
+
message = "Found #{violations.count} #{"violation".pluralize(violations)}:"
|
65
|
+
violations.each do |violation|
|
66
|
+
message += "\n The #{violation} argument is not allowed here. #{unpacked_denylist[violation]}"
|
67
|
+
end
|
68
|
+
|
69
|
+
raise(ArgumentError, message)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
attributes
|
74
|
+
end
|
75
|
+
|
76
|
+
private def validate_attributes(tag:, denylist_name: :attributes_denylist, attributes: {})
|
77
|
+
deny_single_argument(:class, "Use `classes` instead.", attributes)
|
78
|
+
|
79
|
+
if (denylist = attributes[denylist_name])
|
80
|
+
check_denylist(denylist, attributes)
|
81
|
+
|
82
|
+
# Remove :attributes_denylist key and any denied keys from system arguments
|
83
|
+
attributes.except!(denylist_name)
|
84
|
+
attributes.except!(*denylist.keys.flatten)
|
85
|
+
end
|
86
|
+
|
87
|
+
deny_aria_label(tag: tag, attributes: attributes)
|
88
|
+
|
89
|
+
attributes
|
90
|
+
end
|
91
|
+
|
92
|
+
private def deny_single_argument(key, help_text, attributes)
|
93
|
+
raise ArgumentError, "`#{key}` is an invalid argument. #{help_text}" \
|
94
|
+
if should_raise_error? && attributes.key?(key)
|
95
|
+
|
96
|
+
attributes.except!(key)
|
97
|
+
end
|
98
|
+
|
99
|
+
private def deny_aria_label(tag:, attributes:)
|
100
|
+
return attributes.except!(:skip_aria_label_check) if attributes[:skip_aria_label_check]
|
101
|
+
return if attributes[:role]
|
102
|
+
return unless INVALID_ARIA_LABEL_TAGS.include?(tag)
|
103
|
+
|
104
|
+
deny_aria_key(
|
105
|
+
:label,
|
106
|
+
"Don't use `aria-label` on `#{tag}` elements. See https://www.tpgi.com/short-note-on-aria-label-aria-labelledby-and-aria-describedby/",
|
107
|
+
attributes
|
108
|
+
)
|
109
|
+
end
|
110
|
+
|
111
|
+
private def deny_aria_key(key, help_text, attributes)
|
112
|
+
raise ArgumentError, help_text if should_raise_aria_error? && aria(key, attributes)
|
113
|
+
end
|
114
|
+
|
115
|
+
private def should_raise_error?
|
116
|
+
raise_on_invalid_options? && !ENV["ARIADNE_WARNINGS_DISABLED"]
|
117
|
+
end
|
118
|
+
|
119
|
+
private def should_raise_aria_error?
|
120
|
+
raise_on_invalid_aria? && !ENV["ARIADNE_WARNINGS_DISABLED"]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ariadne
|
4
|
+
# Use `Counter` to add a count to navigational elements and buttons.
|
5
|
+
#
|
6
|
+
# @accessibility
|
7
|
+
# Always use `Counter` with adjacent text that provides supplementary information regarding what the count is for. For instance, `Counter`
|
8
|
+
# should be accompanied with text such as `issues` or `pull requests`.
|
9
|
+
#
|
10
|
+
class CounterComponent < Ariadne::Component
|
11
|
+
DEFAULT_CLASSES = "inline-flex items-center p-1 border border-transparent rounded-full shadow-sm text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
|
12
|
+
#
|
13
|
+
# @example Default
|
14
|
+
# <%= render(Ariadne::CounterComponent.new(count: 25)) %>
|
15
|
+
#
|
16
|
+
# @example Schemes
|
17
|
+
# <%= render(Ariadne::CounterComponent.new(count: 25)) %>
|
18
|
+
# <%= render(Ariadne::CounterComponent.new(count: 25)) %>
|
19
|
+
#
|
20
|
+
# @param tag [Symbol, String] The rendered tag name
|
21
|
+
# @param classes [String] <%= link_to_classes_docs %>
|
22
|
+
# @param count [Integer, Float::INFINITY, nil] The number to be displayed (e.x. # of issues, pull requests)
|
23
|
+
# @param limit [Integer, nil] Maximum value to display. Pass `nil` for no limit. (e.x. if `count` == 6,000 and `limit` == 5000, counter will display "5,000+")
|
24
|
+
# @param hide_if_zero [Boolean] If true, a `hidden` attribute is added to the counter if `count` is zero.
|
25
|
+
# @param text [String] Text to display instead of count.
|
26
|
+
# @param round [Boolean] Whether to apply rounding logic to value.
|
27
|
+
# @param attributes [Hash] <%= link_to_attributes_docs %>
|
28
|
+
def initialize(
|
29
|
+
tag: :span,
|
30
|
+
count: 0,
|
31
|
+
limit: 9_000,
|
32
|
+
hide_if_zero: false,
|
33
|
+
text: "",
|
34
|
+
round: false,
|
35
|
+
classes: "",
|
36
|
+
attributes: {}
|
37
|
+
)
|
38
|
+
@count = count
|
39
|
+
@limit = limit
|
40
|
+
@hide_if_zero = hide_if_zero
|
41
|
+
@text = text
|
42
|
+
@round = round
|
43
|
+
@attributes = attributes
|
44
|
+
|
45
|
+
@has_limit = !@limit.nil?
|
46
|
+
|
47
|
+
@tag = check_incoming_tag(:span, tag)
|
48
|
+
|
49
|
+
@attributes[:title] = title
|
50
|
+
|
51
|
+
@classes = class_names(
|
52
|
+
DEFAULT_CLASSES,
|
53
|
+
classes
|
54
|
+
)
|
55
|
+
@attributes[:hidden] = true if count == 0 && hide_if_zero
|
56
|
+
end
|
57
|
+
|
58
|
+
def call
|
59
|
+
render(Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes)) { value }
|
60
|
+
end
|
61
|
+
|
62
|
+
private def title
|
63
|
+
if @text.present?
|
64
|
+
@text
|
65
|
+
elsif @count.nil?
|
66
|
+
"Not available"
|
67
|
+
elsif @count == Float::INFINITY
|
68
|
+
"Infinity"
|
69
|
+
else
|
70
|
+
count = @count.to_i
|
71
|
+
str = number_with_delimiter(@has_limit ? [count, @limit].min : count)
|
72
|
+
str += "+" if @has_limit && count > @limit
|
73
|
+
str
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private def value
|
78
|
+
if @text.present?
|
79
|
+
@text
|
80
|
+
elsif @count.nil?
|
81
|
+
"" # CSS will hide it
|
82
|
+
elsif @count == Float::INFINITY
|
83
|
+
"∞"
|
84
|
+
else
|
85
|
+
if @round
|
86
|
+
count = @has_limit ? [@count.to_i, @limit].min : @count.to_i
|
87
|
+
precision = count.between?(100_000, 999_999) ? 0 : 1
|
88
|
+
units = { thousand: "k", million: "m", billion: "b" }
|
89
|
+
str = number_to_human(count, precision: precision, significant: false, units: units, format: "%n%u")
|
90
|
+
else
|
91
|
+
@count = @count.to_i
|
92
|
+
str = number_with_delimiter(@has_limit ? [@count, @limit].min : @count)
|
93
|
+
end
|
94
|
+
|
95
|
+
str += "+" if @has_limit && @count.to_i > @limit
|
96
|
+
str
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
<%= render Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes) do %>
|
2
|
+
<div class="rounded-md p-4 <%= BG_SCHEME_CLASS_MAPPINGS[@scheme] %>">
|
3
|
+
<div class="flex">
|
4
|
+
<div class="flex-shrink-0">
|
5
|
+
<%= icon %>
|
6
|
+
</div>
|
7
|
+
<div class="ml-3">
|
8
|
+
<div class="mt-2 text-sm <%= CONTENT_SCHEME_CLASS_MAPPINGS[@scheme] %>">
|
9
|
+
<p><%= content %></p>
|
10
|
+
</div>
|
11
|
+
<% if has_action? %>
|
12
|
+
<div class="mt-4 pt-5">
|
13
|
+
<div class="-mx-2 -my-1.5 flex">
|
14
|
+
<%= action %>
|
15
|
+
</div>
|
16
|
+
</div>
|
17
|
+
<% end %>
|
18
|
+
</div>
|
19
|
+
<% if dismissible? %>
|
20
|
+
<div class="pl-3">
|
21
|
+
<div class="-mx-1.5 -my-1.5">
|
22
|
+
<button type="button" class="inline-flex rounded-md p-1.5 focus:outline-none focus:ring-2 focus:ring-offset-2 <%= dismissible_classes %>">
|
23
|
+
<span class="sr-only">Dismiss</span>
|
24
|
+
<%= ariadne_heroicon icon: :x, variant: HeroiconsHelper::Icon::VARIANT_OUTLINE %>
|
25
|
+
</button>
|
26
|
+
</div>
|
27
|
+
</div>
|
28
|
+
<% end %>
|
29
|
+
</div>
|
30
|
+
</div>
|
31
|
+
<% end %>
|