ariadne_view_components 0.0.1 → 0.0.4

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