ariadne_view_components 0.0.1 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -6
  3. data/app/assets/config/manifest.js +2 -0
  4. data/app/assets/javascripts/ariadne_view_components.js +1 -1
  5. data/app/assets/javascripts/ariadne_view_components.js.map +1 -1
  6. data/app/assets/stylesheets/{application.tailwind.css → application.ariadne_view_components.css} +0 -0
  7. data/app/components/ariadne/ariadne.ts +5 -9
  8. data/app/components/ariadne/base_button.rb +1 -0
  9. data/app/components/ariadne/blankslate_component.html.erb +26 -0
  10. data/app/components/ariadne/blankslate_component.rb +151 -0
  11. data/app/components/ariadne/button_component.html.erb +1 -1
  12. data/app/components/ariadne/button_component.rb +4 -1
  13. data/app/components/ariadne/{clipboard_copy_component.ts → clipboard-copy-component.ts} +0 -0
  14. data/app/components/ariadne/clipboard_copy_component.rb +5 -3
  15. data/app/components/ariadne/component.rb +5 -1
  16. data/app/components/ariadne/container_component.html.erb +3 -0
  17. data/app/components/ariadne/container_component.rb +25 -0
  18. data/app/components/ariadne/flash_component.rb +8 -8
  19. data/app/components/ariadne/footer_component.html.erb +7 -0
  20. data/app/components/ariadne/footer_component.rb +23 -0
  21. data/app/components/ariadne/grid_component.html.erb +17 -0
  22. data/app/components/ariadne/grid_component.rb +55 -0
  23. data/app/components/ariadne/header_component.html.erb +29 -0
  24. data/app/components/ariadne/header_component.rb +114 -0
  25. data/app/components/ariadne/heading_component.rb +1 -1
  26. data/app/components/ariadne/heroicon_component.html.erb +4 -5
  27. data/app/components/ariadne/heroicon_component.rb +5 -3
  28. data/app/components/ariadne/image_component.rb +5 -3
  29. data/app/components/ariadne/inline_flex_component.html.erb +5 -0
  30. data/app/components/ariadne/inline_flex_component.rb +63 -0
  31. data/app/components/ariadne/link_component.rb +60 -0
  32. data/app/components/ariadne/list_component.html.erb +17 -0
  33. data/app/components/ariadne/list_component.rb +97 -0
  34. data/app/components/ariadne/pill_component.html.erb +3 -0
  35. data/app/components/ariadne/pill_component.rb +30 -0
  36. data/app/components/ariadne/slideover-component.ts +26 -0
  37. data/app/components/ariadne/slideover_component.html.erb +14 -0
  38. data/app/components/ariadne/slideover_component.rb +77 -0
  39. data/app/components/ariadne/time_ago_component.rb +56 -0
  40. data/app/components/ariadne/time_ago_component.ts +1 -0
  41. data/app/components/ariadne/timeline_component.html.erb +19 -0
  42. data/app/components/ariadne/timeline_component.rb +34 -0
  43. data/app/lib/ariadne/action_view_extensions/form_helper.rb +23 -0
  44. data/app/lib/ariadne/form_builder.rb +71 -0
  45. data/lib/ariadne/classify.rb +9 -1
  46. data/lib/ariadne/view_components/engine.rb +6 -0
  47. data/lib/ariadne/view_components/version.rb +1 -1
  48. data/lib/rubocop/cop/ariadne/ariadne_heroicon.rb +2 -2
  49. data/lib/tasks/docs.rake +48 -58
  50. data/static/arguments.yml +204 -7
  51. data/static/audited_at.json +14 -0
  52. data/static/classes.yml +157 -1
  53. data/static/constants.json +171 -10
  54. data/static/statuses.json +14 -0
  55. metadata +31 -5
  56. data/lib/tasks/tailwind.rake +0 -31
@@ -9,8 +9,8 @@ module Ariadne
9
9
  include IconHelper
10
10
  include HeroiconsHelper
11
11
 
12
- SIZE_DEFAULT = :small
13
- SIZE_MEDIUM = :medium
12
+ SIZE_DEFAULT = :s
13
+ SIZE_MEDIUM = :m
14
14
 
15
15
  SIZE_MAPPINGS = {
16
16
  SIZE_DEFAULT => 16,
@@ -39,6 +39,8 @@ module Ariadne
39
39
  variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
40
40
  { name: "mail",
41
41
  variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
42
+ { name: "menu",
43
+ variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
42
44
  { name: "pencil",
43
45
  variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
44
46
  { name: "plus-sm",
@@ -60,7 +62,7 @@ module Ariadne
60
62
  # <%= render(Ariadne::HeroiconComponent.new(icon: :check, variant: HeroiconsHelper::Icon::VARIANT_SOLID)) %>
61
63
  #
62
64
  # @example Medium
63
- # <%= render(Ariadne::HeroiconComponent.new(icon: :"user-group", variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, size: :medium)) %>
65
+ # <%= render(Ariadne::HeroiconComponent.new(icon: :"user-group", variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, size: :m)) %>
64
66
  #
65
67
  # @example Helper
66
68
  # <%= ariadne_heroicon(icon: :check, variant: HeroiconsHelper::Icon::VARIANT_OUTLINE) %>
@@ -6,6 +6,8 @@ module Ariadne
6
6
  # @accessibility
7
7
  # Always provide a meaningful `alt`.
8
8
  class ImageComponent < Ariadne::Component
9
+ DEFAULT_TAG = :img
10
+
9
11
  # @example Default
10
12
  #
11
13
  # <%= render(Ariadne::ImageComponent.new(src: "https://github.com/github.png", alt: "GitHub")) %>
@@ -23,16 +25,16 @@ module Ariadne
23
25
  # <%= render(Ariadne::ImageComponent.new(src: "https://github.com/github.png", alt: "GitHub", attributes: { height: 100, width: 100 })) %>
24
26
  #
25
27
  # @param tag [Symbol, String] The rendered tag name
26
- # @param classes [String] <%= link_to_classes_docs %>
27
28
  # @param src [String] The source url of the image.
28
29
  # @param alt [String] Specifies an alternate text for the image.
29
30
  # @param lazy [Boolean] Whether or not to lazily load the image.
31
+ # @param classes [String] <%= link_to_classes_docs %>
30
32
  # @param attributes [Hash] <%= link_to_attributes_docs %>
31
- def initialize(tag: :img, src:, alt:, lazy: false, classes: "", attributes: {})
33
+ def initialize(tag: DEFAULT_TAG, src:, alt:, lazy: false, classes: "", attributes: {})
32
34
  @attributes = attributes
33
35
 
34
36
  @src = src
35
- @tag = check_incoming_tag(:img, tag)
37
+ @tag = check_incoming_tag(DEFAULT_TAG, tag)
36
38
  @classes = classes
37
39
 
38
40
  @attributes[:alt] = alt
@@ -0,0 +1,5 @@
1
+ <%= render(Ariadne::BaseComponent.new(tag: :span, classes: @classes, attributes: @attributes)) do |component| %>
2
+ <%= icon %>
3
+ <%= item %>
4
+ <%= text %>
5
+ <% end %>
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ariadne
4
+ # Represents two items side-by-side. Typically, this will be an icon (of CSS classes, SVG, or a Heroicon icon)
5
+ # with optional text.
6
+ #
7
+ # InlineFlexComponent differs from HeroiconComponent in that it is intended to be
8
+ # used within (or next to) text, whereas HeroiconComponent is intended to only
9
+ # present a static list of SVG images (and can be embedded in buttons or shown alone).
10
+ class InlineFlexComponent < Ariadne::Component
11
+ DEFAULT_TAG = :span
12
+ DEFAULT_CLASSES = "inline-flex items-baseline"
13
+
14
+ STATE_OPTIONS = [:closed, :open].freeze
15
+
16
+ STATE_OPEN_SVG = <<~MSG
17
+ <svg viewBox="0 0 24 24" width="12" height="12" class="stroke-state-open" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
18
+ <circle cx="12" cy="12" r="10"></circle>
19
+ </svg>
20
+ MSG
21
+ STATE_CLOSED_SVG = <<~MSG
22
+ <svg viewBox="0 0 24 24" width="12" height="12" class="stroke-state-closed fill-state-closed " stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
23
+ <circle cx="12" cy="12" r="10"></circle>
24
+ </svg>
25
+ MSG
26
+
27
+ renders_one :icon, lambda { |icon, tag: :svg, variant:, size: Ariadne::HeroiconComponent::SIZE_DEFAULT, classes: "", attributes: {}|
28
+ Ariadne::HeroiconComponent.new(tag: tag, icon: icon, variant: variant, size: size, classes: classes, attributes: attributes)
29
+ }
30
+
31
+ renders_one :item, lambda { |tag: :span, classes: "", attributes: {}|
32
+ Ariadne::BaseComponent.new(tag: tag, classes: classes, attributes: attributes) { content }
33
+ }
34
+
35
+ DEFAULT_TEXT_OPEN_CLASSES = "text-state-open"
36
+ DEFAULT_TEXT_CLOSED_CLASSES = "text-state-closed"
37
+ DEFAULT_TEXT_CLASSES = "pl-2 text-sm font-medium text-gray-900 text-sm"
38
+ renders_one :text, lambda { |classes: "", attributes: {}|
39
+ actual_classes = class_names(DEFAULT_TEXT_CLASSES, classes)
40
+ Ariadne::BaseComponent.new(tag: :span, classes: actual_classes, attributes: attributes) { content }
41
+ }
42
+
43
+ # @example Default
44
+ #
45
+ # <%= render(Ariadne::InlineFlexComponent.new) do |c| %>
46
+ # <% c.item { Ariadne::InlineFlexComponent::STATE_OPEN_SVG.html_safe } %>
47
+ # <% end %>
48
+ #
49
+ # <%= render(Ariadne::InlineFlexComponent.new) do |c| %>
50
+ # <% c.icon(:check, size: :s, variant: HeroiconsHelper::Icon::VARIANT_SOLID) %>
51
+ # <% c.text { "Closed" } %>
52
+ # <% end %>
53
+ #
54
+ # @param tag [Symbol, String] The rendered tag name
55
+ # @param classes [String] <%= link_to_classes_docs %>
56
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
57
+ def initialize(tag: DEFAULT_TAG, classes: "", attributes: {})
58
+ @tag = check_incoming_tag(DEFAULT_TAG, tag)
59
+ @classes = class_names(DEFAULT_CLASSES, classes)
60
+ @attributes = attributes
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ariadne
4
+ # Use `Link` for navigating from one page to another. `Link` styles anchor tags with default styling and hover text-decoration.
5
+ class LinkComponent < Ariadne::Component
6
+ DEFAULT_TAG = :a
7
+ TAG_OPTIONS = [DEFAULT_TAG, :span].freeze
8
+
9
+ # `Tooltip` that appears on mouse hover or keyboard focus over the link. Use tooltips sparingly and as a last resort.
10
+ # Consult the <%= link_to_component(Ariadne::TooltipComponent) %> documentation for more information.
11
+ #
12
+ # @param tag [Symbol, String] The rendered tag name
13
+ # @param text [String] The text content of the tooltip. This should be brief and no longer than a sentence.
14
+ # @param classes [String] <%= link_to_classes_docs %>
15
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
16
+ renders_one :tooltip, lambda { |tag: :"tool-tip", text:, classes: "", attributes: {}|
17
+ raise ArgumentError, "Links with a tooltip must have a unique `id` set on the `LinkComponent`." if @id.blank?
18
+
19
+ actual_classes = class_names(classes)
20
+
21
+ Ariadne::TooltipComponent.new(tag: tag, text: text, classes: actual_classes, attributes: attributes)
22
+ }
23
+
24
+ # @example Default
25
+ # <%= render(Ariadne::LinkComponent.new(href: "#")) { "Link" } %>
26
+ #
27
+ # @example Span as link
28
+ # <%= render(Ariadne::LinkComponent.new(tag: :span, href: "#")) { "Span as a link" } %>
29
+ #
30
+ # @example With tooltip
31
+ # @description
32
+ # Use tooltips sparingly and as a last resort. Consult the <%= link_to_component(Ariadne::TooltipComponent) %> documentation for more information.
33
+ # @code
34
+ # <%= render(Ariadne::LinkComponent.new(href: "#", attributes: { id: "link-with-tooltip" })) do |c| %>
35
+ # <% c.tooltip(text: "Tooltip text") %>
36
+ # Link
37
+ # <% end %>
38
+ #
39
+ # @param tag [String] <%= one_of(Ariadne::LinkComponent::TAG_OPTIONS) %>
40
+ # @param href [String] URL to be used for the link.
41
+ # @param classes [String] <%= link_to_classes_docs %>
42
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
43
+ def initialize(tag: DEFAULT_TAG, href:, classes: "", attributes: {})
44
+ @tag = check_incoming_tag(DEFAULT_TAG, tag)
45
+
46
+ @attributes = attributes
47
+ @attributes[:href] = href
48
+
49
+ @id = @attributes[:id]
50
+
51
+ @classes = classes
52
+ end
53
+
54
+ def call
55
+ render(Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes)) do
56
+ content.to_s + tooltip.to_s
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,17 @@
1
+ <%= render Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes) do |list| %>
2
+ <% items.each do |item| %>
3
+ <% if item.linked? %>
4
+ <li data-highlight-target="highlightable" class="relative <%= selected? ? 'bg-button-bg-color' : 'hover:bg-button-hover-color' %> focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
5
+ <%= render(item.link) do %>
6
+ <%= item.header %>
7
+ <%= item.entry %>
8
+ <% end %>
9
+ </li>
10
+ <% else %>
11
+ <li data-highlight-target="highlightable" class="relative <%= selected? ? 'bg-button-bg-color' : 'hover:bg-button-hover-color' %> focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
12
+ <%= item.header %>
13
+ <%= item.entry %>
14
+ </li>
15
+ <% end %>
16
+ <% end %>
17
+ <% end %>
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ariadne
4
+ # `List` is used to show a list of items in a vertical format.
5
+ class ListComponent < Ariadne::Component
6
+ DEFAULT_UL_CLASSES = "divide-y divide-gray-300"
7
+
8
+ renders_many :items, "Item"
9
+
10
+ # @example Basic
11
+ # <% numbers = [1, 2, 3] %>
12
+ # <%= render(Ariadne::ListComponent.new) do |list| %>
13
+ # <% numbers.each do |number| %>
14
+ # <%= list.item do |item| %>
15
+ # <%= item.entry { number } %>
16
+ # <% end %>
17
+ # <% end %>
18
+ # <% end %>
19
+ #
20
+ # @example With a header
21
+ # <% numbers = [1, 2, 3] %>
22
+ # <%= render(Ariadne::ListComponent.new) do |list| %>
23
+ # <% numbers.each_with_index do |number, idx| %>
24
+ # <%= list.item do |item| %>
25
+ # <%= item.header { idx } %>
26
+ # <%= item.entry { number } %>
27
+ # <% end %>
28
+ # <% end %>
29
+ # <% end %>
30
+ #
31
+ # @param classes [String] <%= link_to_classes_docs %>
32
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
33
+ def initialize(classes: "", attributes: {})
34
+ @tag = :ul
35
+ @classes = class_names(DEFAULT_UL_CLASSES, classes)
36
+ @attributes = attributes
37
+ @attributes[:"data-controller"] = "highlight"
38
+ end
39
+
40
+ def selected?
41
+ false
42
+ end
43
+
44
+ def render?
45
+ items.any?
46
+ end
47
+
48
+ # This component is part of `ListComponent` and should not be
49
+ # used as a standalone component.
50
+ class Item < Ariadne::Component
51
+ renders_one :header, lambda { |static_content = nil, &block|
52
+ next static_content if static_content.present?
53
+
54
+ render(Ariadne::BaseComponent.new(tag: :div, classes: "flex justify-between")) do
55
+ view_context.capture { block&.call }
56
+ end
57
+ }
58
+
59
+ renders_one :entry, lambda { |static_content = nil, &block|
60
+ next static_content if static_content.present?
61
+
62
+ render(Ariadne::BaseComponent.new(tag: :div, classes: "mt-1")) do
63
+ view_context.capture { block&.call }
64
+ end
65
+ }
66
+
67
+ attr_accessor :selected
68
+ attr_reader :link, :classes, :attributes
69
+
70
+ def initialize(link: {}, classes: "", attributes: {})
71
+ if link.present?
72
+ @link = Ariadne::LinkComponent.new(**link)
73
+ end
74
+
75
+ @classes = classes
76
+ @attributes = attributes
77
+ @selected = false
78
+ end
79
+
80
+ def selected?
81
+ @selected
82
+ end
83
+
84
+ def linked?
85
+ @link.present?
86
+ end
87
+
88
+ def call
89
+ if selected
90
+ link_arguments[:"aria-current"] = "page"
91
+ end
92
+
93
+ Ariadne::BaseComponent.new(tag: :div, classes: @classes, attributes: @attributes)
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,3 @@
1
+ <%= render Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes) do %>
2
+ <%= content %>
3
+ <% end %>
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ariadne
4
+ # Creates a rounded label that resembles a medicine pill.
5
+ class PillComponent < Ariadne::Component
6
+ DEFAULT_TAG = :span
7
+ TAG_OPTIONS = [DEFAULT_TAG].freeze
8
+
9
+ DEFAULT_CLASSES = "flex-shrink-0 inline-block px-2 py-0.5 text-xs font-medium rounded-full"
10
+
11
+ # @example Default
12
+ #
13
+ # <%= render(Ariadne::PillComponent.new(color: "FF0000")) { "Admin" } %>
14
+ #
15
+ # @param tag [Symbol, String] The rendered tag name.
16
+ # @param color [String] The hex color of the pill.
17
+ # @param classes [String] <%= link_to_classes_docs %>
18
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
19
+ def initialize(tag: DEFAULT_TAG, color:, classes: "", attributes: {})
20
+ @tag = check_incoming_tag(DEFAULT_TAG, tag)
21
+ @classes = class_names(
22
+ DEFAULT_CLASSES,
23
+ classes
24
+ )
25
+
26
+ @attributes = attributes
27
+ @attributes["style"] = "background-color: ##{color};"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ import {Controller} from '@hotwired/stimulus'
2
+
3
+ export default class SlideoverComponent extends Controller {
4
+ static targets = ['expandable', 'expandWrapper', 'slidePanel', 'buttonWrapper']
5
+
6
+ declare readonly expandableTarget: HTMLDivElement
7
+ declare readonly expandWrapperTarget: HTMLDivElement
8
+ declare readonly slidePanelTargets: [HTMLDivElement]
9
+ declare readonly buttonWrapperTarget: HTMLDivElement
10
+
11
+ toggle() {
12
+ this.expandableTarget.classList.toggle('hidden')
13
+ this.expandWrapperTarget.classList.toggle('bg-filter-panel')
14
+ for (const slidePanel of this.slidePanelTargets) {
15
+ slidePanel.classList.toggle('hidden')
16
+ }
17
+ this.buttonWrapperTarget.classList.toggle('bg-filter-panel')
18
+ if (document.getElementById('btnClose')?.classList.contains('hidden')) {
19
+ const formID = this.buttonWrapperTarget.getAttribute('data-slideover-component-form-id')
20
+ if (formID) {
21
+ const form = <HTMLFormElement>document.getElementById(formID)
22
+ form?.submit()
23
+ }
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,14 @@
1
+ <%= render Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes) do |slideover| %>
2
+ <div data-slideover-component-target="expandWrapper" class="flex flex-col">
3
+ <div data-slideover-component-target="expandable" class="hidden bg-filter-panel px-3 pb-4">
4
+ <%= content %>
5
+ </div>
6
+ <div class="relative top-4 inset-0 flex flex-row items-center bg-filter-panel" aria-hidden="true">
7
+ <div class="w-full border-t border-billy-purple z-10"></div>
8
+ </div>
9
+ <div data-slideover-component-target="buttonWrapper" data-slideover-component-form-id="<%= @form_id %>" class="relative flex justify-center">
10
+ <%= open_button %>
11
+ <%= close_button %>
12
+ </div>
13
+ </div>
14
+ <% end %>
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ariadne
4
+ # Add a general description of component here
5
+ # Add additional usage considerations or best practices that may aid the user to use the component correctly.
6
+ # @accessibility Add any accessibility considerations
7
+ class SlideoverComponent < Ariadne::Component
8
+ DEFAULT_TAG = :div
9
+ TAG_OPTIONS = [DEFAULT_TAG].freeze
10
+
11
+ DIRECTION_Y = :y
12
+ DIRECTION_X = :x
13
+ VALID_DIRECTIONS = [DIRECTION_Y, DIRECTION_X].freeze
14
+
15
+ DEFAULT_CLASSES = ""
16
+
17
+ DEFAULT_BUTTON_CLASSES = "inline-flex items-center shadow-sm px-4 py-1.5 border border-our-purple-300 z-50 text-sm leading-5 font-medium rounded-full text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
18
+ renders_one :open_button, lambda { |text:, classes: "", attributes: {}|
19
+ actual_classes = class_names(DEFAULT_BUTTON_CLASSES, classes)
20
+
21
+ attributes[:id] ||= "btnOpen"
22
+ attributes[:"data-slideover-component-target"] ||= "slidePanel"
23
+ attributes[:"data-action"] ||= "click->slideover-component#toggle"
24
+ attributes[:type] ||= "submit"
25
+
26
+ render(Ariadne::BaseComponent.new(tag: :button, classes: actual_classes, attributes: attributes)) do
27
+ render(Ariadne::InlineFlexComponent.new(attributes: { "data-slideover-component-target" => "slide-panel" })) do |flex|
28
+ flex.icon(:"chevron-double-down", size: :s, variant: HeroiconsHelper::Icon::VARIANT_SOLID)
29
+ flex.text { text }
30
+ end
31
+ end
32
+ }
33
+
34
+ renders_one :close_button, lambda { |text:, classes: "", attributes: {}|
35
+ actual_classes = class_names(DEFAULT_BUTTON_CLASSES, classes)
36
+
37
+ attributes[:id] ||= "btnClose"
38
+ attributes[:"data-slideover-component-target"] ||= "slidePanel"
39
+ attributes[:"data-action"] ||= "click->slideover-component#toggle"
40
+ attributes[:type] ||= "submit"
41
+
42
+ render(Ariadne::BaseComponent.new(tag: :button, classes: actual_classes, attributes: attributes)) do
43
+ render(Ariadne::InlineFlexComponent.new(attributes: { "data-slideover-component-target" => "slide-panel" })) do |flex|
44
+ flex.icon(:"chevron-double-up", size: :s, variant: HeroiconsHelper::Icon::VARIANT_SOLID)
45
+ flex.text { text }
46
+ end
47
+ end
48
+ }
49
+
50
+ # @example Default
51
+ #
52
+ # <%= render(Ariadne::SlideoverComponent.new(direction: Ariadne::SlideoverComponent::DIRECTION_Y)) { "Example" } %>
53
+ #
54
+ # @param tag [Symbol, String] The rendered tag name.
55
+ # @param direction [Symbol] <%= one_of(Ariadne::SlideoverComponent::VALID_DIRECTIONS) %>
56
+ # @param form_id [String] The ID of any <form> tag to submit when the button is clicked.
57
+ # @param open_text [String] The text to use to open the slideover.
58
+ # @param close_text [String] The text to use to close the slideover.
59
+ # @param classes [String] <%= link_to_classes_docs %>
60
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
61
+ def initialize(tag: DEFAULT_TAG, direction:, form_id: nil, open_text: "Open", close_text: "Close", classes: "", attributes: {})
62
+ @tag = check_incoming_tag(DEFAULT_TAG, tag)
63
+ @classes = class_names(
64
+ DEFAULT_CLASSES,
65
+ classes
66
+ )
67
+
68
+ @direction = fetch_or_raise(VALID_DIRECTIONS, direction)
69
+ @form_id = form_id
70
+
71
+ @open_text = open_text
72
+ @close_text = close_text
73
+ @attributes = attributes
74
+ @attributes[:"data-controller"] = "slideover-component"
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ariadne
4
+ # Displays a time relative to how long ago it was. This component requires JavaScript.
5
+ class TimeAgoComponent < Ariadne::Component
6
+ DEFAULT_TAG = :"time-ago"
7
+ TAG_OPTIONS = [DEFAULT_TAG].freeze
8
+
9
+ DEFAULT_CLASSES = "whitespace-nowrap"
10
+
11
+ # @example Default
12
+ #
13
+ # <%= render(Ariadne::TimeAgoComponent.new(time: Time.now)) %>
14
+ #
15
+ # @param tag [Symbol, String] The rendered tag name.
16
+ # @param time [Time] The time to be formatted
17
+ # @param micro [Boolean] If true then the text will be formatted in "micro" mode, using as few characters as possible
18
+ # @param classes [String] <%%= link_to_classes_docs %>
19
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
20
+ def initialize(tag: DEFAULT_TAG, time:, micro: false, classes: "", attributes: {})
21
+ @tag = check_incoming_tag(DEFAULT_TAG, tag)
22
+ @classes = class_names(
23
+ DEFAULT_CLASSES,
24
+ classes
25
+ )
26
+
27
+ @time = time
28
+ @micro = micro
29
+ @attributes = attributes
30
+ @attributes[:datetime] = @time.utc.iso8601
31
+ @attributes[:format] = "micro" if @micro
32
+ end
33
+
34
+ def call
35
+ render(Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes)) { time_in_words }
36
+ end
37
+
38
+ private def time_in_words
39
+ return @time.in_time_zone.strftime("%b %-d, %Y") unless @micro
40
+
41
+ seconds_ago = Time.current - @time
42
+
43
+ if seconds_ago < 1.minute
44
+ "1m"
45
+ elsif seconds_ago >= 1.minute && seconds_ago < 1.hour
46
+ "#{(seconds_ago / 60).floor}m"
47
+ elsif seconds_ago >= 1.hour && seconds_ago < 1.day
48
+ "#{(seconds_ago / 60 / 60).floor}h"
49
+ elsif seconds_ago >= 1.day && seconds_ago < 1.year
50
+ "#{(seconds_ago / 60 / 60 / 24).floor}d"
51
+ elsif seconds_ago >= 1.year
52
+ "#{(seconds_ago / 60 / 60 / 24 / 365).floor}y"
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1 @@
1
+ import '@github/time-elements'
@@ -0,0 +1,19 @@
1
+ <%= render Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes) do %>
2
+ <div>
3
+ <div class="divide-y divide-gray-200">
4
+ <div class="pb-4">
5
+ <h2 id="activity-title" class="text-lg font-medium text-gray-900">Timeline</h2>
6
+ </div>
7
+ <div class="pt-6">
8
+ <!-- Activity feed-->
9
+ <div class="flow-root">
10
+ <ul role="list" class="-mb-8">
11
+ <% items.each do %>
12
+ <%= items %>
13
+ <% end %>
14
+ </ul>
15
+ </div>
16
+ </div>
17
+ </div>
18
+ </div>
19
+ <% end %>
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ariadne
4
+ # Represents a linear timeline of events. Typically, this is shown
5
+ # as part of the Conversation component.
6
+ class TimelineComponent < Ariadne::Component
7
+ DEFAULT_TAG = :div
8
+ DEFAULT_CLASSES = ""
9
+
10
+ # The sub-items(s) to render
11
+ renders_many :items, lambda { |static_content = nil, &block|
12
+ next static_content if static_content.present?
13
+
14
+ view_context.capture { block&.call }
15
+ }
16
+
17
+ # @example Default
18
+ #
19
+ # <%= render(Ariadne::TimelineComponent.new) { "Example" } %>
20
+ #
21
+ # @param tag [Symbol, String] The rendered tag name
22
+ # @param classes [String] <%= link_to_classes_docs %>
23
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
24
+ def initialize(tag: DEFAULT_TAG, classes: "", attributes: {})
25
+ @tag = check_incoming_tag(DEFAULT_TAG, tag)
26
+ @classes = class_names(
27
+ DEFAULT_CLASSES,
28
+ classes
29
+ )
30
+
31
+ @attributes = attributes
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ariadne
4
+ module ActionViewExtensions
5
+ # :nodoc:
6
+ module FormHelper
7
+ include ClassNameHelper
8
+
9
+ DEFAULT_FORM_CLASSES = "space-y-8 divide-y divide-gray-200 sm:space-y-5"
10
+ def ariadne_form_with(model: nil, scope: nil, url: nil, format: nil, classes: {}, attributes: {}, **options, &block)
11
+ options[:class] = class_names(DEFAULT_FORM_CLASSES, options[:class])
12
+ options[:builder] ||= Ariadne::FormBuilder
13
+ options[:html] ||= {}
14
+ options = options.merge(attributes)
15
+ form_with(model: model, scope: scope, url: url, format: format, **options, &block)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ ActiveSupport.on_load(:action_view) do
22
+ include Ariadne::ActionViewExtensions::FormHelper
23
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ariadne
4
+ # :nodoc:
5
+ class FormBuilder < ActionView::Helpers::FormBuilder
6
+ include ClassNameHelper
7
+
8
+ DEFAULT_SECTION_CLASSES = "divide-y divide-gray-200 pt-8 space-y-6 sm:pt-10 sm:space-y-5"
9
+ def section(classes: "", attributes: {}, &block)
10
+ actual_classes = class_names(DEFAULT_SECTION_CLASSES, classes)
11
+ options = { class: actual_classes, **attributes }
12
+ @template.content_tag(:div, **options, &block)
13
+ end
14
+
15
+ DEFAULT_SECTION_HEADING_CLASSES = "text-lg leading-6 font-medium text-gray-900"
16
+ def heading(classes: "", attributes: {}, &block)
17
+ actual_classes = class_names(DEFAULT_SECTION_HEADING_CLASSES, classes)
18
+ options = { class: actual_classes, **attributes }
19
+ @template.content_tag(:h3, **options, &block)
20
+ end
21
+
22
+ DEFAULT_SECTION_SUBHEADING_CLASSES = "mt-1 max-w-2xl text-sm text-gray-500"
23
+ def subheading(classes: "", attributes: {}, &block)
24
+ actual_classes = class_names(DEFAULT_SECTION_SUBHEADING_CLASSES, classes)
25
+ options = { class: actual_classes, **attributes }
26
+ @template.content_tag(:p, **options, &block)
27
+ end
28
+
29
+ DEFAULT_LABEL_CLASSES = "block text-sm font-medium text-gray-700"
30
+ def label(method, text = nil, options = {}, &block)
31
+ options[:class] = class_names(DEFAULT_LABEL_CLASSES, options[:class])
32
+ super(method, **options)
33
+ end
34
+
35
+ DEFAULT_TEXT_CLASSES = "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md"
36
+ def text_field(method, options = {})
37
+ options[:class] = class_names(DEFAULT_TEXT_CLASSES, options[:class])
38
+ super(method, **options)
39
+ end
40
+
41
+ DEFAULT_CHECKBOX_CLASSES = "focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
42
+ def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
43
+ options[:class] = class_names(DEFAULT_TEXT_CLASSES, options[:class])
44
+ super(method, **options)
45
+ end
46
+
47
+ DEFAULT_RADIO_CLASSES = "focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
48
+ def radio_button(method, tag_value, options = {})
49
+ options[:class] = class_names(DEFAULT_RADIO_CLASSES, options[:class])
50
+ super(method, tag_value, **options)
51
+ end
52
+
53
+ DEFAULT_TEXTAREA_CLASSES = "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border border-gray-300 rounded-md"
54
+ def text_area(method, options = {})
55
+ options[:class] = class_names(DEFAULT_TEXTAREA_CLASSES, options[:class])
56
+ super(method, **options)
57
+ end
58
+
59
+ DEFAULT_EMAIL_CLASSES = "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md"
60
+ def email_field(method, options = {})
61
+ options[:class] = class_names(DEFAULT_EMAIL_CLASSES, options[:class])
62
+ super(method, **options)
63
+ end
64
+
65
+ DEFAULT_PASSWORD_CLASSES = "appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
66
+ def password_field(method, options = {})
67
+ options[:class] = class_names(DEFAULT_PASSWORD_CLASSES, options[:class])
68
+ super(method, **options)
69
+ end
70
+ end
71
+ end