pulse_view_components 0.3.0

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 (221) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +88 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +78 -0
  5. data/app/assets/builds/js/pulse_view_components.js +9543 -0
  6. data/app/assets/builds/js/pulse_view_components.js.map +7 -0
  7. data/app/assets/builds/pulse_view_components.css +1 -0
  8. data/app/assets/config/manifest.js +2 -0
  9. data/app/assets/images/pulse_view_components/icons/alert.svg +3 -0
  10. data/app/assets/images/pulse_view_components/icons/archive.svg +1 -0
  11. data/app/assets/images/pulse_view_components/icons/arrow-both.svg +1 -0
  12. data/app/assets/images/pulse_view_components/icons/arrow-down.svg +1 -0
  13. data/app/assets/images/pulse_view_components/icons/arrow-left.svg +1 -0
  14. data/app/assets/images/pulse_view_components/icons/arrow-right.svg +1 -0
  15. data/app/assets/images/pulse_view_components/icons/arrow-switch.svg +1 -0
  16. data/app/assets/images/pulse_view_components/icons/arrow-up.svg +1 -0
  17. data/app/assets/images/pulse_view_components/icons/beaker.svg +1 -0
  18. data/app/assets/images/pulse_view_components/icons/bell-slash.svg +1 -0
  19. data/app/assets/images/pulse_view_components/icons/bell.svg +1 -0
  20. data/app/assets/images/pulse_view_components/icons/bookmark-slash.svg +1 -0
  21. data/app/assets/images/pulse_view_components/icons/bookmark.svg +1 -0
  22. data/app/assets/images/pulse_view_components/icons/calendar.svg +1 -0
  23. data/app/assets/images/pulse_view_components/icons/check-circle.svg +1 -0
  24. data/app/assets/images/pulse_view_components/icons/check.svg +1 -0
  25. data/app/assets/images/pulse_view_components/icons/chevron-down.svg +1 -0
  26. data/app/assets/images/pulse_view_components/icons/circle.svg +1 -0
  27. data/app/assets/images/pulse_view_components/icons/clock.svg +1 -0
  28. data/app/assets/images/pulse_view_components/icons/close.svg +9 -0
  29. data/app/assets/images/pulse_view_components/icons/cloud.svg +1 -0
  30. data/app/assets/images/pulse_view_components/icons/comment-discussion.svg +1 -0
  31. data/app/assets/images/pulse_view_components/icons/comment.svg +1 -0
  32. data/app/assets/images/pulse_view_components/icons/copy.svg +1 -0
  33. data/app/assets/images/pulse_view_components/icons/database.svg +1 -0
  34. data/app/assets/images/pulse_view_components/icons/dot.svg +1 -0
  35. data/app/assets/images/pulse_view_components/icons/dots-outline.svg +1 -0
  36. data/app/assets/images/pulse_view_components/icons/download.svg +1 -0
  37. data/app/assets/images/pulse_view_components/icons/duplicate.svg +1 -0
  38. data/app/assets/images/pulse_view_components/icons/eye.svg +1 -0
  39. data/app/assets/images/pulse_view_components/icons/file-added.svg +1 -0
  40. data/app/assets/images/pulse_view_components/icons/file-directory.svg +1 -0
  41. data/app/assets/images/pulse_view_components/icons/file-media.svg +1 -0
  42. data/app/assets/images/pulse_view_components/icons/file.svg +1 -0
  43. data/app/assets/images/pulse_view_components/icons/filter.svg +1 -0
  44. data/app/assets/images/pulse_view_components/icons/flame.svg +1 -0
  45. data/app/assets/images/pulse_view_components/icons/gear.svg +1 -0
  46. data/app/assets/images/pulse_view_components/icons/gift.svg +1 -0
  47. data/app/assets/images/pulse_view_components/icons/globe.svg +1 -0
  48. data/app/assets/images/pulse_view_components/icons/goal.svg +1 -0
  49. data/app/assets/images/pulse_view_components/icons/graph.svg +1 -0
  50. data/app/assets/images/pulse_view_components/icons/heart-fill.svg +1 -0
  51. data/app/assets/images/pulse_view_components/icons/heart.svg +1 -0
  52. data/app/assets/images/pulse_view_components/icons/history.svg +1 -0
  53. data/app/assets/images/pulse_view_components/icons/home.svg +1 -0
  54. data/app/assets/images/pulse_view_components/icons/hourglass.svg +1 -0
  55. data/app/assets/images/pulse_view_components/icons/image.svg +1 -0
  56. data/app/assets/images/pulse_view_components/icons/inbox.svg +1 -0
  57. data/app/assets/images/pulse_view_components/icons/info.svg +1 -0
  58. data/app/assets/images/pulse_view_components/icons/issue.svg +3 -0
  59. data/app/assets/images/pulse_view_components/icons/key.svg +1 -0
  60. data/app/assets/images/pulse_view_components/icons/law.svg +1 -0
  61. data/app/assets/images/pulse_view_components/icons/link-external.svg +1 -0
  62. data/app/assets/images/pulse_view_components/icons/link.svg +1 -0
  63. data/app/assets/images/pulse_view_components/icons/location.svg +1 -0
  64. data/app/assets/images/pulse_view_components/icons/log.svg +1 -0
  65. data/app/assets/images/pulse_view_components/icons/mail.svg +1 -0
  66. data/app/assets/images/pulse_view_components/icons/mark-github.svg +1 -0
  67. data/app/assets/images/pulse_view_components/icons/mark-jira.svg +3 -0
  68. data/app/assets/images/pulse_view_components/icons/megaphone.svg +1 -0
  69. data/app/assets/images/pulse_view_components/icons/no-entry.svg +1 -0
  70. data/app/assets/images/pulse_view_components/icons/note.svg +1 -0
  71. data/app/assets/images/pulse_view_components/icons/organization.svg +1 -0
  72. data/app/assets/images/pulse_view_components/icons/package.svg +1 -0
  73. data/app/assets/images/pulse_view_components/icons/paintbrush.svg +1 -0
  74. data/app/assets/images/pulse_view_components/icons/paperclip.svg +1 -0
  75. data/app/assets/images/pulse_view_components/icons/paste.svg +1 -0
  76. data/app/assets/images/pulse_view_components/icons/pencil.svg +1 -0
  77. data/app/assets/images/pulse_view_components/icons/people.svg +1 -0
  78. data/app/assets/images/pulse_view_components/icons/person-add.svg +1 -0
  79. data/app/assets/images/pulse_view_components/icons/person.svg +1 -0
  80. data/app/assets/images/pulse_view_components/icons/pin-slash.svg +1 -0
  81. data/app/assets/images/pulse_view_components/icons/pin.svg +1 -0
  82. data/app/assets/images/pulse_view_components/icons/play.svg +1 -0
  83. data/app/assets/images/pulse_view_components/icons/plus-circle.svg +1 -0
  84. data/app/assets/images/pulse_view_components/icons/plus.svg +1 -0
  85. data/app/assets/images/pulse_view_components/icons/project.svg +1 -0
  86. data/app/assets/images/pulse_view_components/icons/pulse.svg +1 -0
  87. data/app/assets/images/pulse_view_components/icons/question.svg +1 -0
  88. data/app/assets/images/pulse_view_components/icons/redo.svg +1 -0
  89. data/app/assets/images/pulse_view_components/icons/reply.svg +1 -0
  90. data/app/assets/images/pulse_view_components/icons/report.svg +1 -0
  91. data/app/assets/images/pulse_view_components/icons/rss.svg +1 -0
  92. data/app/assets/images/pulse_view_components/icons/ruby.svg +1 -0
  93. data/app/assets/images/pulse_view_components/icons/search.svg +1 -0
  94. data/app/assets/images/pulse_view_components/icons/share.svg +1 -0
  95. data/app/assets/images/pulse_view_components/icons/shield.svg +1 -0
  96. data/app/assets/images/pulse_view_components/icons/sliders.svg +1 -0
  97. data/app/assets/images/pulse_view_components/icons/smiley.svg +1 -0
  98. data/app/assets/images/pulse_view_components/icons/sort.svg +1 -0
  99. data/app/assets/images/pulse_view_components/icons/sort_asc.svg +1 -0
  100. data/app/assets/images/pulse_view_components/icons/sort_desc.svg +1 -0
  101. data/app/assets/images/pulse_view_components/icons/stack.svg +1 -0
  102. data/app/assets/images/pulse_view_components/icons/star-fill.svg +1 -0
  103. data/app/assets/images/pulse_view_components/icons/star.svg +1 -0
  104. data/app/assets/images/pulse_view_components/icons/stopwatch.svg +1 -0
  105. data/app/assets/images/pulse_view_components/icons/sun.svg +1 -0
  106. data/app/assets/images/pulse_view_components/icons/sync.svg +1 -0
  107. data/app/assets/images/pulse_view_components/icons/table.svg +1 -0
  108. data/app/assets/images/pulse_view_components/icons/tag.svg +1 -0
  109. data/app/assets/images/pulse_view_components/icons/tasklist.svg +1 -0
  110. data/app/assets/images/pulse_view_components/icons/telescope.svg +1 -0
  111. data/app/assets/images/pulse_view_components/icons/three-bars.svg +1 -0
  112. data/app/assets/images/pulse_view_components/icons/thumbsdown.svg +1 -0
  113. data/app/assets/images/pulse_view_components/icons/thumbsup.svg +1 -0
  114. data/app/assets/images/pulse_view_components/icons/tools.svg +1 -0
  115. data/app/assets/images/pulse_view_components/icons/trash.svg +1 -0
  116. data/app/assets/images/pulse_view_components/icons/trophy.svg +1 -0
  117. data/app/assets/images/pulse_view_components/icons/undo.svg +1 -0
  118. data/app/assets/images/pulse_view_components/icons/unlink.svg +1 -0
  119. data/app/assets/images/pulse_view_components/icons/upload.svg +1 -0
  120. data/app/assets/images/pulse_view_components/icons/x-circle.svg +1 -0
  121. data/app/assets/images/pulse_view_components/icons/x.svg +1 -0
  122. data/app/assets/images/pulse_view_components/icons/zap.svg +1 -0
  123. data/app/assets/javascripts/pulse_view_components.js +0 -0
  124. data/app/assets/stylesheets/pulse_view_components/application.tailwind.css +10 -0
  125. data/app/components/pulse/action_list/divider.rb +35 -0
  126. data/app/components/pulse/action_list/form_wrapper.html.slim +9 -0
  127. data/app/components/pulse/action_list/form_wrapper.rb +63 -0
  128. data/app/components/pulse/action_list/heading.rb +52 -0
  129. data/app/components/pulse/action_list/item.html.slim +43 -0
  130. data/app/components/pulse/action_list/item.rb +319 -0
  131. data/app/components/pulse/action_list.css +568 -0
  132. data/app/components/pulse/action_list.rb +261 -0
  133. data/app/components/pulse/badge.rb +59 -0
  134. data/app/components/pulse/base_component.rb +36 -0
  135. data/app/components/pulse/blank_slate.html.slim +10 -0
  136. data/app/components/pulse/blank_slate.rb +19 -0
  137. data/app/components/pulse/blank_slate.yml +2 -0
  138. data/app/components/pulse/border_box/header.html.slim +4 -0
  139. data/app/components/pulse/border_box/header.rb +48 -0
  140. data/app/components/pulse/border_box.html.slim +10 -0
  141. data/app/components/pulse/border_box.rb +124 -0
  142. data/app/components/pulse/box.rb +17 -0
  143. data/app/components/pulse/breadcrumb_item.rb +40 -0
  144. data/app/components/pulse/breadcrumbs.html.slim +9 -0
  145. data/app/components/pulse/breadcrumbs.rb +17 -0
  146. data/app/components/pulse/button.rb +148 -0
  147. data/app/components/pulse/component.rb +99 -0
  148. data/app/components/pulse/conditional_wrapper.rb +21 -0
  149. data/app/components/pulse/content.rb +11 -0
  150. data/app/components/pulse/counter.css +10 -0
  151. data/app/components/pulse/counter.rb +107 -0
  152. data/app/components/pulse/flash.html.slim +9 -0
  153. data/app/components/pulse/flash.rb +52 -0
  154. data/app/components/pulse/forms/error_explanation.html.slim +8 -0
  155. data/app/components/pulse/forms/error_explanation.rb +36 -0
  156. data/app/components/pulse/icon.rb +30 -0
  157. data/app/components/pulse/label.html.slim +5 -0
  158. data/app/components/pulse/label.rb +42 -0
  159. data/app/components/pulse/layout.css +239 -0
  160. data/app/components/pulse/layout.html.slim +7 -0
  161. data/app/components/pulse/layout.rb +141 -0
  162. data/app/components/pulse/link.html.slim +7 -0
  163. data/app/components/pulse/link.rb +52 -0
  164. data/app/components/pulse/modal.html.slim +51 -0
  165. data/app/components/pulse/modal.rb +64 -0
  166. data/app/components/pulse/modal_controller.js +77 -0
  167. data/app/components/pulse/nav_link.html.slim +16 -0
  168. data/app/components/pulse/nav_link.rb +25 -0
  169. data/app/components/pulse/nav_list/divider.rb +13 -0
  170. data/app/components/pulse/nav_list/group.html.slim +6 -0
  171. data/app/components/pulse/nav_list/group.rb +110 -0
  172. data/app/components/pulse/nav_list/heading.rb +36 -0
  173. data/app/components/pulse/nav_list/item.rb +171 -0
  174. data/app/components/pulse/nav_list.html.slim +11 -0
  175. data/app/components/pulse/nav_list.rb +225 -0
  176. data/app/components/pulse/popover.html.slim +5 -0
  177. data/app/components/pulse/popover.rb +88 -0
  178. data/app/components/pulse/progress_bar.html.slim +5 -0
  179. data/app/components/pulse/progress_bar.rb +23 -0
  180. data/app/components/pulse/progress_bar.yml +2 -0
  181. data/app/components/pulse/table/cell.html.slim +10 -0
  182. data/app/components/pulse/table/cell.rb +90 -0
  183. data/app/components/pulse/table/cell.yml +5 -0
  184. data/app/components/pulse/table/column.rb +22 -0
  185. data/app/components/pulse/table.html.slim +50 -0
  186. data/app/components/pulse/table.rb +111 -0
  187. data/app/components/pulse/table.yml +2 -0
  188. data/app/components/pulse/text.rb +72 -0
  189. data/app/components/pulse/toast.html.slim +14 -0
  190. data/app/components/pulse/toast.rb +85 -0
  191. data/app/components/pulse/toast_controller.js +32 -0
  192. data/app/javascript/pulse_view_components/application.js +24 -0
  193. data/app/javascript/pulse_view_components.js +4 -0
  194. data/app/lib/pulse/attributes_helper.rb +107 -0
  195. data/app/lib/pulse/fetch_or_fallback_helper.rb +53 -0
  196. data/app/lib/pulse/svg_helper.rb +28 -0
  197. data/lib/pulse/view_components/engine.rb +74 -0
  198. data/lib/pulse/view_components/version.rb +17 -0
  199. data/lib/pulse/view_components.rb +14 -0
  200. data/lib/pulse_view_components.rb +3 -0
  201. data/lib/tasks/pulse_view_components_tasks.rake +8 -0
  202. data/previews/pulse/badge_preview.rb +51 -0
  203. data/previews/pulse/blank_slate_preview.rb +14 -0
  204. data/previews/pulse/border_box_preview.rb +48 -0
  205. data/previews/pulse/breadcrumbs_preview.rb +13 -0
  206. data/previews/pulse/button_preview.rb +137 -0
  207. data/previews/pulse/counter_preview.rb +14 -0
  208. data/previews/pulse/flash_preview.rb +85 -0
  209. data/previews/pulse/icon_preview.rb +12 -0
  210. data/previews/pulse/label_preview.rb +10 -0
  211. data/previews/pulse/layout_preview.rb +34 -0
  212. data/previews/pulse/link_preview.rb +55 -0
  213. data/previews/pulse/modal_preview.rb +48 -0
  214. data/previews/pulse/nav_link_preview.rb +48 -0
  215. data/previews/pulse/nav_list_preview.rb +138 -0
  216. data/previews/pulse/popover_preview.rb +77 -0
  217. data/previews/pulse/progress_bar_preview.rb +23 -0
  218. data/previews/pulse/table_preview.rb +168 -0
  219. data/previews/pulse/text_preview.rb +102 -0
  220. data/previews/pulse/toast_preview.rb +67 -0
  221. metadata +344 -0
@@ -0,0 +1,6 @@
1
+ /! components/pulse/nav_list/group
2
+ - with_post_list_content do
3
+ - if show_more_item?
4
+ = show_more_item
5
+
6
+ = render_parent
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pulse
4
+ class NavList
5
+ # A logical grouping of navigation links with an optional heading.
6
+ #
7
+ # See <%= link_to_component(Pulse::NavList) %> for usage examples.
8
+ class Group < ActionList
9
+ # A special "show more" list item that appears at the bottom of the group. Clicking
10
+ # the item will fetch the next page of results from the URL passed in the `src` argument
11
+ # and append the resulting chunk of HTML to the group.
12
+ #
13
+ # @param src [String] The URL to query for additional pages of list items.
14
+ # @param pages [Integer] The total number of pages in the result set.
15
+ # @param component_klass [Class] A component class to use instead of the default `Pulse::NavList::Item` class.
16
+ # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Pulse::NavList::Item) %>.
17
+ renders_one :show_more_item, lambda { |src:, pages:,
18
+ component_klass: NavList::Item,
19
+ **system_arguments|
20
+ system_arguments[:classes] = merge_classes(
21
+ @item_classes,
22
+ system_arguments[:classes]
23
+ )
24
+ system_arguments[:tag] = :div
25
+ system_arguments[:id] ||= self.class.generate_id(base_name: 'item')
26
+ system_arguments[:hidden] = true
27
+ system_arguments[:href] = '#'
28
+ system_arguments[:data] ||= {}
29
+ system_arguments[:data][:target] = 'nav-list.showMoreItem'
30
+ system_arguments[:data][:action] = 'click:nav-list#showMore'
31
+ system_arguments[:data][:current_page] = '1'
32
+ system_arguments[:data][:total_pages] = pages.to_s
33
+ system_arguments[:label_arguments] = {
34
+ **system_arguments[:label_arguments] || {},
35
+ color: :accent
36
+ }
37
+
38
+ system_arguments[:content_arguments] = {
39
+ **system_arguments[:content_arguments] || {},
40
+ tag: :button
41
+ }
42
+
43
+ system_arguments[:content_arguments][:data] = merge_data(
44
+ system_arguments[:content_arguments],
45
+ data: { list_id: id }
46
+ )
47
+
48
+ component_klass.new(list: self, src:, **system_arguments)
49
+ }
50
+
51
+ # @private
52
+ def self.custom_element_name
53
+ Pulse::NavList.custom_element_name
54
+ end
55
+
56
+ # @param selected_item_id [Symbol] The ID of the currently selected item. Used internally.
57
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
58
+ def initialize(selected_item_id: nil, **system_arguments)
59
+ @system_arguments = system_arguments
60
+ @selected_item_id = selected_item_id
61
+
62
+ super(**@system_arguments)
63
+ end
64
+
65
+ # Cause this group to show its list of sub items when rendered.
66
+ # simplecov:disable
67
+ def expand!
68
+ @expanded = true
69
+ end
70
+ # simplecov:enable
71
+
72
+ # @!parse
73
+ # # Items.
74
+ # #
75
+ # # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Pulse::NavList::Item) %>.
76
+ # renders_many :items
77
+
78
+ # @private
79
+ def build_item(component_klass: NavList::Item, **system_arguments)
80
+ super(
81
+ component_klass:,
82
+ selected_item_id: @selected_item_id,
83
+ **system_arguments
84
+ )
85
+ end
86
+
87
+ # @private
88
+ def build_avatar_item(component_klass: NavList::Item, **system_arguments)
89
+ super(
90
+ component_klass:,
91
+ selected_item_id: @selected_item_id,
92
+ **system_arguments
93
+ )
94
+ end
95
+
96
+ def kind
97
+ :group
98
+ end
99
+
100
+ def before_render
101
+ super
102
+
103
+ return if heading?
104
+
105
+ raise ArgumentError,
106
+ 'NavList groups are required to have headings'
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pulse
4
+ class NavList
5
+ # The heading placed above a `NavList`'s items.
6
+ #
7
+ # See <%= link_to_component(NavList) %> for usage examples.
8
+ class Heading < Pulse::Component
9
+ attr_reader :title, :id, :heading_level, :system_arguments
10
+
11
+ # @param title [String] The text content of the heading.
12
+ # @param id [String] The value of the ID HTML attribute. Auto-generated by default.
13
+ # @param heading_level [Integer] The heading level, i.e. 2 for an `<h2>`, 3 for an `<h3>`, etc.
14
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
15
+ def initialize(title:, id: self.class.generate_id, heading_level: 2,
16
+ **system_arguments)
17
+ @title = title
18
+ @id = id
19
+ @heading_level = heading_level
20
+ @system_arguments = system_arguments
21
+ end
22
+
23
+ def call
24
+ render(
25
+ Pulse::Text.new(
26
+ type: :body4,
27
+ tag: :"h#{heading_level}",
28
+ id:,
29
+ classes: 'pulse-ml-2 pulse-mb-4',
30
+ **system_arguments
31
+ ).with_content(title)
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pulse
4
+ class NavList
5
+ # Items are rendered as styled links. They can optionally include leading and/or trailing visuals,
6
+ # such as icons, avatars, and counters. Items are selected by ID. IDs can be specified via the
7
+ # `selected_item_ids` argument, which accepts a list of valid IDs for the item. Items can also
8
+ # themselves contain sub items. Sub items are rendered collapsed by default.
9
+ class Item < Pulse::ActionList::Item
10
+ attr_reader :selected_by_ids, :sub_item
11
+
12
+ # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Pulse::ActionList::Item) %>.
13
+ renders_many :items, lambda { |**system_arguments|
14
+ raise 'Items can only be nested 2 levels deep' if sub_item?
15
+
16
+ @list.build_item(parent: self,
17
+ sub_item: true,
18
+ **system_arguments).tap do |item|
19
+ @list.will_add_item(item)
20
+ end
21
+ }
22
+
23
+ # Whether or not this item is nested under a parent item.
24
+ #
25
+ # @return [Boolean]
26
+ alias sub_item? sub_item
27
+
28
+ # @param selected_item_id [Symbol] The ID of the currently selected list item. Used internally.
29
+ # @param selected_by_ids [Array<Symbol>] The list of IDs that select this item. In other words, if the `selected_item_id` attribute on the parent `NavList` is set to one of these IDs, the item will appear selected.
30
+ # @param expanded [Boolean] Whether this item shows (expands) or hides (collapses) its list of sub items.
31
+ # @param sub_item [Boolean] Whether or not this item is nested under a parent item. Used internally.
32
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
33
+ def initialize(selected_item_id: nil, selected_by_ids: [],
34
+ sub_item: false, expanded: false, **system_arguments)
35
+ @selected_item_id = selected_item_id
36
+ @selected_by_ids = Array(selected_by_ids)
37
+ @expanded = expanded
38
+ @sub_item = sub_item
39
+
40
+ system_arguments[:classes] = merge_classes(
41
+ system_arguments[:classes],
42
+ 'ActionListItem--subItem' => @sub_item
43
+ )
44
+
45
+ @sub_list_arguments = {
46
+ classes: merge_classes(
47
+ 'ActionList',
48
+ 'ActionList--subGroup'
49
+ )
50
+ }
51
+
52
+ @list = system_arguments[:list]
53
+
54
+ if @list
55
+ @sub_list_arguments['data-action'] =
56
+ "keydown:#{@list.custom_element_name}#handleItemWithSubItemKeydown"
57
+ end
58
+
59
+ overrides = { 'data-item-id': @selected_by_ids.join(' ') }
60
+
61
+ super(**system_arguments, **overrides)
62
+ end
63
+
64
+ def active?
65
+ item_active?(self) && items.empty?
66
+ end
67
+
68
+ # Cause this item to show its list of sub items when rendered.
69
+ def expand!
70
+ @expanded = true
71
+ end
72
+
73
+ def before_render
74
+ if active_sub_item?
75
+ expand!
76
+
77
+ @content_arguments[:classes] = merge_classes(
78
+ @content_arguments[:classes],
79
+ 'ActionListContent--hasActiveSubItem'
80
+ )
81
+ else
82
+ @system_arguments[:classes] = merge_classes(
83
+ @system_arguments[:classes],
84
+ 'pulse-outline-none pulse-bg-gray-50' => active?
85
+ )
86
+ end
87
+
88
+ @content_arguments[:'aria-current'] = 'page' if active?
89
+
90
+ super
91
+
92
+ if items.present? && trailing_action.present?
93
+ raise 'Cannot render a trailing action for an item with subitems'
94
+ end
95
+
96
+ if items.present? && @selected_by_ids.present?
97
+ raise 'Cannot pass `selected_by_ids:` for an item with subitems, ' \
98
+ 'since parent items cannot be selected'
99
+ end
100
+
101
+ return if items.blank?
102
+
103
+ @sub_list_arguments[:aria] = merge_aria(
104
+ @sub_list_arguments,
105
+ { aria: { labelledby: id } }
106
+ )
107
+
108
+ if href.present?
109
+ raise ArgumentError,
110
+ 'Items with sub-items cannot have hrefs'
111
+ end
112
+
113
+ @content_arguments[:tag] = :button
114
+ @content_arguments[:'aria-expanded'] = @expanded.to_s
115
+ @content_arguments[:'data-action'] = "
116
+ click:#{@list.custom_element_name}#handleItemWithSubItemClick
117
+ keydown:#{@list.custom_element_name}#handleItemWithSubItemKeydown
118
+ "
119
+
120
+ with_private_trailing_action_icon(
121
+ :'chevron-down',
122
+ classes: 'ActionListItem-collapseIcon'
123
+ )
124
+
125
+ @system_arguments[:classes] = merge_classes(
126
+ @system_arguments[:classes],
127
+ 'ActionListItem--hasSubItem'
128
+ )
129
+ end
130
+
131
+ def kind
132
+ :item
133
+ end
134
+
135
+ private
136
+
137
+ # Normally it would be easier to simply ask each item for its active status, eg.
138
+ # items.any?(&:active?), but unfortunately the view context is not set on each
139
+ # item until _after_ the parent's before_render, etc methods have been called.
140
+ # This means helper methods like current_page? will blow up with an error, since
141
+ # `helpers` is simply an alias for the view context (i.e. an instance of
142
+ # ActionView::Base). Since we know the view context for the parent object must
143
+ # be set before `before_render` is invoked, we can call helper methods here in
144
+ # the parent and bypass the problem entirely. Maybe not the most OO approach,
145
+ # but it works.
146
+ def item_active?(item)
147
+ if item.selected_by_ids.present?
148
+ item.selected_by_ids.include?(@selected_item_id)
149
+ elsif item.href
150
+ current_page?(item.href)
151
+ else
152
+ # simplecov:disable
153
+ false
154
+ # simplecov:enable
155
+ end
156
+ end
157
+
158
+ def active_sub_item?
159
+ items.any? { |subitem| item_active?(subitem) }
160
+ end
161
+
162
+ def current_page?(url)
163
+ helpers.current_page?(url)
164
+ end
165
+
166
+ def list_class
167
+ Pulse::NavList
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,11 @@
1
+ /! components/pulse/nav_list
2
+ = render Pulse::BaseComponent.new(tag: :nav, **@system_arguments) do
3
+ = heading
4
+ nav-list
5
+ = render(Pulse::ConditionalWrapper.new(condition: render_outer_list?, tag: :ul)) do
6
+ - items.each_with_index do |item, index|
7
+ - if index > 0 && render_divider_between?(item, items[index - 1])
8
+ = render(Pulse::ActionList::Divider.new)
9
+
10
+ = render(Pulse::ConditionalWrapper.new(condition: render_outer_list? && group?(item), tag: :li)) do
11
+ = item
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pulse
4
+ # `NavList` provides a simple way to render side navigation, i.e. navigation
5
+ # that appears to the left or right side of some main content. Each group in a
6
+ # nav list is a list of links.
7
+ #
8
+ # Nav list groups can contain sub items. Rather than navigating to a URL, groups
9
+ # with sub items expand and collapse on click. To indicate this functionality, the
10
+ # group will automatically render with a trailing chevron icon that changes direction
11
+ # when the group expands and collapses.
12
+ #
13
+ # Nav list items appear visually active when selected. Each nav item must have one
14
+ # or more ID values that determine which item will appear selected. Use the
15
+ # `selected_item_id` argument to select the appropriate item.
16
+ class NavList < Pulse::Component
17
+ # @private
18
+ def self.custom_element_name
19
+ 'nav-list'
20
+ end
21
+
22
+ # The heading for the list at large. Accepts the arguments accepted by <%= link_to_component(Pulse::NavList::Heading) %>.
23
+ #
24
+ renders_one :heading, Pulse::NavList::Heading
25
+
26
+ # @!parse
27
+ # # Adds an item to the list.
28
+ # #
29
+ # # @param component_klass [Class] The class to use instead of the default <%= link_to_component(Pulse::NavList::Item) %>
30
+ # # @param system_arguments [Hash] These arguments are forwarded to <%= link_to_component(Pulse::NavList::Item) %>, or whatever class is passed as the `component_klass` argument.
31
+ # def with_item(component_klass: Pulse::NavList::Item, **system_arguments, &block)
32
+ # end
33
+
34
+ # @!parse
35
+ # # Adds an avatar item to the list. Avatar items are a convenient way to accessibly add an item with a leading avatar image.
36
+ # #
37
+ # # @param src [String] The source url of the avatar image.
38
+ # # @param username [String] The username associated with the avatar.
39
+ # # @param full_name [String] Optional. The user's full name.
40
+ # # @param full_name_scheme [Symbol] Optional. How to display the user's full name. <%= one_of(ActionList::Item::DESCRIPTION_SCHEME_OPTIONS) %>
41
+ # # @param component_klass [Class] The class to use instead of the default <%= link_to_component(Pulse::NavList::Item) %>
42
+ # # @param avatar_arguments [Hash] Optional. The arguments accepted by <%= link_to_component(Pulse::Avatar) %>
43
+ # # @param system_arguments [Hash] These arguments are forwarded to <%= link_to_component(Pulse::NavList::Item) %>, or whatever class is passed as the `component_klass` argument.
44
+ # def with_avatar_item(src:, username:, full_name: nil, full_name_scheme: ActionList::Item::DEFAULT_DESCRIPTION_SCHEME, component_klass: Pulse::NavList::Item, avatar_arguments: {}, **system_arguments, &block)
45
+ # end
46
+
47
+ # @!parse
48
+ # # Adds a group to the list. A group is a list of links and a (required) heading.
49
+ # #
50
+ # # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Pulse::NavList::Group) %>.
51
+ # def with_group(**system_arguments, &block)
52
+ # end
53
+
54
+ # @!parse
55
+ # # Adds a divider to the list. Dividers visually separate items and groups.
56
+ # #
57
+ # # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Pulse::NavList::Divider) %>.
58
+ # def with_divider(**system_arguments, &block)
59
+ # end
60
+
61
+ # Items. Items can be individual items, dividers, or groups. See the documentation for `#with_item`, `#with_divider`, and `#with_group` respectively for more information.
62
+ #
63
+ renders_many :items, types: {
64
+ item: {
65
+ renders: lambda { |**system_arguments, &block|
66
+ build_item(**system_arguments, &block)
67
+ },
68
+
69
+ as: :item
70
+ },
71
+
72
+ avatar_item: {
73
+ renders: lambda { |**system_arguments|
74
+ build_avatar_item(**system_arguments)
75
+ },
76
+
77
+ as: :avatar_item
78
+ },
79
+
80
+ divider: {
81
+ renders: Pulse::NavList::Divider,
82
+ as: :divider
83
+ },
84
+
85
+ group: {
86
+ renders: lambda { |**system_arguments, &block|
87
+ Pulse::NavList::Group.new(
88
+ selected_item_id: @selected_item_id,
89
+ **system_arguments,
90
+ &block
91
+ )
92
+ },
93
+
94
+ as: :group
95
+ }
96
+ }
97
+
98
+ # @param selected_item_id [Symbol] The ID of the currently selected item. The default is `nil`, meaning no item is selected.
99
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
100
+ def initialize(selected_item_id: nil, **system_arguments)
101
+ @system_arguments = system_arguments
102
+ @selected_item_id = selected_item_id
103
+ end
104
+
105
+ # Builds a new item but does not add it to the list. Use this method
106
+ # instead of the `#with_item` slot if you need to render an item outside
107
+ # the context of a list, eg. if rendering additional items to append to
108
+ # an existing list, perhaps via a separate HTTP request.
109
+ #
110
+ # @param component_klass [Class] The class to use instead of the default <%= link_to_component(Pulse::NavList::Item) %>
111
+ # @param system_arguments [Hash] These arguments are forwarded to <%= link_to_component(Pulse::NavList::Item) %>, or whatever class is passed as the `component_klass` argument.
112
+ def build_item(component_klass: Pulse::NavList::Item, **system_arguments,
113
+ &)
114
+ component_klass.new(
115
+ list: top_level_group,
116
+ selected_item_id: @selected_item_id,
117
+ **system_arguments,
118
+ &
119
+ )
120
+ end
121
+
122
+ # Builds a new avatar item but does not add it to the list. Avatar items
123
+ # are a convenient way to accessibly add an item with a leading avatar
124
+ # image. Use this method instead of the `#with_avatar_item` slot if you
125
+ # need to render an avatar item outside the context of a list, eg. if
126
+ # rendering additional items to append to an existing list, perhaps via
127
+ # a separate HTTP request.
128
+ #
129
+ # @param src [String] The source url of the avatar image.
130
+ # @param username [String] The username associated with the avatar.
131
+ # @param full_name [String] Optional. The user's full name.
132
+ # @param full_name_scheme [Symbol] Optional. How to display the user's full name. <%= one_of(ActionList::ItemComponent::DESCRIPTION_SCHEME_OPTIONS) %>
133
+ # @param component_klass [Class] The class to use instead of the default <%= link_to_component(Pulse::NavList::Item) %>
134
+ # @param avatar_arguments [Hash] Optional. The arguments accepted by <%= link_to_component(Pulse::Avatar) %>
135
+ # @param system_arguments [Hash] These arguments are forwarded to <%= link_to_component(Pulse::NavList::Item) %>, or whatever class is passed as the `component_klass` argument.
136
+ def build_avatar_item(
137
+ src:, username:, full_name: nil,
138
+ full_name_scheme: ActionList::Item::DEFAULT_DESCRIPTION_SCHEME,
139
+ component_klass: Pulse::NavList::Item, avatar_arguments: {},
140
+ **system_arguments
141
+ )
142
+ component_klass.new(
143
+ list: top_level_group,
144
+ selected_item_id: @selected_item_id,
145
+ label: username,
146
+ description_scheme: full_name_scheme,
147
+ **system_arguments
148
+ ).tap do |item|
149
+ item.with_leading_visual_raw_content do
150
+ # no alt text necessary
151
+ item.render(Pulse::Avatar.new(src:, **avatar_arguments,
152
+ role: :presentation, size: 16))
153
+ end
154
+
155
+ item.with_description_content(full_name) if full_name
156
+ end
157
+ end
158
+
159
+ private
160
+
161
+ def before_render
162
+ if heading?
163
+ if aria(
164
+ :label, @system_arguments
165
+ )
166
+ raise ArgumentError,
167
+ "Please don't set an aria-label if a heading is provided"
168
+ end
169
+
170
+ @system_arguments[:aria] = merge_aria(
171
+ @system_arguments,
172
+ { aria: { labelledby: heading.id } }
173
+ )
174
+ else
175
+ unless aria(
176
+ :label, @system_arguments
177
+ )
178
+ raise ArgumentError,
179
+ 'When no heading is provided, an aria-label must be given'
180
+ end
181
+ end
182
+ end
183
+
184
+ # Lists that contain top-level items (i.e. items outside of a group) should be wrapped in a <ul>
185
+ def render_outer_list?
186
+ items.any? { |item| !group?(item) }
187
+ end
188
+
189
+ def render_divider_between?(item1, item2)
190
+ return false if either_is_divider?(item1, item2)
191
+
192
+ both_are_groups?(item1, item2) || heterogeneous?(item1, item2)
193
+ end
194
+
195
+ def both_are_groups?(item1, item2)
196
+ group?(item1) && group?(item2)
197
+ end
198
+
199
+ def heterogeneous?(item1, item2)
200
+ kind(item1) != kind(item2)
201
+ end
202
+
203
+ def either_is_divider?(item1, item2)
204
+ divider?(item1) || divider?(item2)
205
+ end
206
+
207
+ def group?(item)
208
+ kind(item) == :group
209
+ end
210
+
211
+ def divider?(item)
212
+ kind(item) == :divider
213
+ end
214
+
215
+ def kind(item)
216
+ item.respond_to?(:kind) ? item.kind : :item
217
+ end
218
+
219
+ def top_level_group
220
+ # dummy group for the list: argument in the item slot above
221
+ @top_level_group ||=
222
+ Pulse::NavList::Group.new(selected_item_id: @selected_item_id)
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,5 @@
1
+ /! components/pulse/popover
2
+ = render Pulse::BaseComponent.new(**@system_arguments) do
3
+ = render(body_component) do
4
+ = heading
5
+ = body
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pulse
4
+ # Use `Popover` to bring attention to specific user interface elements,
5
+ # typically to suggest an action or to guide users through a new experience.
6
+ #
7
+ # By default, the popover renders with absolute positioning, meaning it should
8
+ # usually be wrapped in an element with a relative position in order to be
9
+ # positioned properly. To render the popover with relative positioning, use
10
+ # the relative property.
11
+ class Popover < Pulse::Component
12
+ CARET_DEFAULT = :top
13
+ CARET_MAPPINGS = {
14
+ CARET_DEFAULT => 'pulse-caret-top',
15
+ :bottom => 'pulse-caret-bottom',
16
+ :bottom_right => 'Popover-message--bottom-right',
17
+ :bottom_left => 'Popover-message--bottom-left',
18
+ :left => 'pulse-caret-left',
19
+ :left_bottom => 'Popover-message--left-bottom',
20
+ :left_top => 'Popover-message--left-top',
21
+ :right => 'pulse-caret-right',
22
+ :right_bottom => 'Popover-message--right-bottom',
23
+ :right_top => 'Popover-message--right-top',
24
+ :top_left => 'Popover-message--top-left',
25
+ :top_right => 'Popover-message--top-right'
26
+ }.freeze
27
+
28
+ DEFAULT_HEADING_TAG = :h4
29
+
30
+ # The heading
31
+ #
32
+ # @param tag [Symbol] (Pulse::Popover::DEFAULT_HEADING_TAG) <%= one_of(Pulse::Heading::TAG_OPTIONS) %>
33
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
34
+ renders_one :heading, lambda { |tag: DEFAULT_HEADING_TAG,
35
+ **system_arguments|
36
+ system_arguments[:type] = :body3
37
+ system_arguments[:tag] = tag
38
+ system_arguments[:classes] = merge_classes(
39
+ 'pulse-mb-2',
40
+ system_arguments[:classes]
41
+ )
42
+
43
+ Pulse::Text.new(**system_arguments)
44
+ }
45
+
46
+ # The body
47
+ #
48
+ # @param caret [Symbol] <%= one_of(Pulse::Popover::CARET_MAPPINGS.keys) %>
49
+ # @param large [Boolean] Whether to use the large version of the component.
50
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
51
+ renders_one :body, lambda { |caret: CARET_DEFAULT, large: false,
52
+ **system_arguments|
53
+ @body_arguments = system_arguments
54
+ @body_arguments['data-caret'] = caret
55
+ @body_arguments[:classes] = merge_classes(
56
+ @body_arguments[:classes],
57
+ CARET_MAPPINGS[fetch_or_fallback(CARET_MAPPINGS.keys, caret,
58
+ CARET_DEFAULT)],
59
+ 'Popover-message--large' => large
60
+ )
61
+ @body_arguments[:classes] = merge_classes(
62
+ @body_arguments[:classes],
63
+ 'pulse-bg-white pulse-border pulse-border-gray-300 pulse-rounded-lg',
64
+ 'pulse-p-4 pulse-mt-2 pulse-mx-auto pulse-text-left pulse-shadow-lg'
65
+ )
66
+
67
+ Pulse::Content.new
68
+ }
69
+
70
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
71
+ def initialize(**system_arguments)
72
+ @system_arguments = deny_tag_argument(**system_arguments)
73
+ @system_arguments[:tag] = :div
74
+ @system_arguments[:classes] = merge_classes(
75
+ 'pulse-relative pulse-z-10 pulse-w-64',
76
+ system_arguments[:classes]
77
+ )
78
+ end
79
+
80
+ def render?
81
+ body.present?
82
+ end
83
+
84
+ def body_component
85
+ Pulse::Box.new(**@body_arguments)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,5 @@
1
+ /! components/pulse/progress_bar
2
+ = render Pulse::BaseComponent.new(**@system_attributes) do
3
+ div class="pulse-h-1 pulse-bg-success" style="width: #{[@percentage, 100].min}%"
4
+ - if @percentage > 100
5
+ div class="pulse-h-1 pulse-bg-error" style="width: #{@percentage - 100}%"
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pulse
4
+ # Renders a simple progress bar
5
+ class ProgressBar < Pulse::Component
6
+ def initialize(percentage: 0, **system_attributes)
7
+ @percentage = percentage.to_i
8
+ @system_attributes = system_attributes
9
+ @system_attributes[:tag] ||= :div
10
+ @system_attributes[:class] = merge_classes(
11
+ 'pulse-flex pulse-flex-row pulse-h-1 pulse-w-full pulse-bg-gray-300',
12
+ 'dark:pulse-bg-gray-600',
13
+ system_attributes[:classes]
14
+ )
15
+ end
16
+
17
+ private
18
+
19
+ def before_render
20
+ @system_attributes[:title] ||= t('.title', percentage: @percentage)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,2 @@
1
+ en:
2
+ title: "%{percentage}% of budgeted hours used"