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,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pulse
4
+ # Renders a button or link button
5
+ class Button < Pulse::Component
6
+ attr_reader :kwargs, :label, :url, :tag, :button_to_helper,
7
+ :disabled
8
+
9
+ delegate :button_to, to: :helpers
10
+
11
+ DEFAULT_SCHEME = :secondary
12
+ SCHEME_MAPPINGS = {
13
+ primary: 'pulse-border pulse-border-solid pulse-border-gray-300 ' \
14
+ 'pulse-bg-green-700 pulse-text-white hover:pulse-bg-green-800 ' \
15
+ 'focus-visible:pulse-bg-green-800 hover:pulse-text-white ' \
16
+ 'focus-visible:pulse-text-white ' \
17
+ 'focus-visible:pulse-shadow-btn-inset',
18
+ secondary: 'pulse-border pulse-border-solid pulse-border-gray-300 ' \
19
+ 'pulse-bg-gray-50 pulse-text-gray hover:pulse-bg-gray-100 ' \
20
+ 'focus-visible:pulse-bg-gray-100',
21
+ danger: 'pulse-border pulse-border-solid pulse-border-gray-300 ' \
22
+ 'pulse-bg-gray-50 pulse-text-danger-800 ' \
23
+ 'hover:pulse-bg-danger-800 focus-visible:pulse-bg-danger-800 ' \
24
+ 'hover:pulse-text-white focus-visible:pulse-text-white ' \
25
+ 'focus-visible:pulse-shadow-btn-inset'
26
+ }.freeze
27
+ SCHEME_OPTIONS = SCHEME_MAPPINGS.keys
28
+
29
+ DEFAULT_SIZE = :md
30
+ SIZE_MAPPINGS = {
31
+ xs: 'pulse-text-xs pulse-leading-5 pulse-font-medium pulse-py-0.5 ' \
32
+ 'pulse-px-4',
33
+ sm: 'pulse-text-sm pulse-font-medium pulse-py-1 pulse-px-5',
34
+ md: 'pulse-text-base pulse-font-bold pulse-py-1.5 pulse-px-6'
35
+ }.freeze
36
+ SIZE_OPTIONS = SIZE_MAPPINGS.keys
37
+
38
+ # Leading visuals appear to the left of the button text.
39
+ #
40
+ # Use:
41
+ #
42
+ # - `leading_visual_icon` for a <%= link_to_component(Ui::Icon) %>.
43
+ #
44
+ # @param system_arguments [Hash] Same arguments as <%= link_to_component(Ui::Icon) %>.
45
+ renders_one :leading_visual, types: {
46
+ icon: lambda { |**system_arguments|
47
+ system_arguments[:classes] = merge_classes(
48
+ 'pulse-me-2',
49
+ system_arguments[:classes]
50
+ )
51
+
52
+ Pulse::Icon.new(**system_arguments)
53
+ }
54
+ }
55
+
56
+ def initialize(label: nil, scheme: DEFAULT_SCHEME, size: DEFAULT_SIZE,
57
+ url: nil, button_to: false, disabled: false, **kwargs)
58
+ @kwargs = kwargs
59
+ @label = label
60
+ @scheme = scheme
61
+ @url = url
62
+ @size = size
63
+ @button_to_helper = button_to
64
+ @disabled = disabled
65
+ @tag = kwargs.fetch(:tag, :button)
66
+ end
67
+
68
+ def options
69
+ @options ||= begin
70
+ values = kwargs.except(:style, :tag, :label, :url, :size, :classes)
71
+
72
+ if tag == :button && url.blank?
73
+ # Default button to "type" to prevent the HTML5 default "submit"
74
+ # behavior
75
+ # See: https://stackoverflow.com/questions/932653/how-to-prevent-buttons-from-submitting-forms
76
+ values = values.merge(type: kwargs.fetch(:type, 'button'))
77
+ end
78
+
79
+ values.merge(class: btn_class)
80
+ end
81
+ end
82
+
83
+ def btn_class
84
+ merge_classes(
85
+ 'pulse-appearance-none pulse-inline-flex pulse-justify-center',
86
+ 'pulse-items-center pulse-rounded-md pulse-no-underline',
87
+ 'hover:pulse-no-underline focus-visible:pulse-no-underline',
88
+ 'focus-visible:-pulse-outline-offset-2',
89
+ 'focus-visible:pulse-outline-[2px] focus-visible:pulse-outline-solid',
90
+ 'focus-visible:pulse-outline-[var(--pulse-focus-outlineColor)]',
91
+ 'disabled:pulse-opacity-25 disabled:pulse-cursor-not-allowed',
92
+ SIZE_MAPPINGS[@size],
93
+ SCHEME_MAPPINGS[@scheme],
94
+ kwargs.fetch(:classes, '')
95
+ )
96
+ end
97
+
98
+ def render_link
99
+ if label.present?
100
+ link_to(helpers.safe_join([leading_visual, label]), url, options)
101
+ else
102
+ link_to(url, options) { helpers.safe_join([leading_visual, content]) }
103
+ end
104
+ end
105
+
106
+ def render_button
107
+ if label.present?
108
+ content_tag(tag, helpers.safe_join([leading_visual, label]), options)
109
+ else
110
+ content_tag(tag, options) do
111
+ helpers.safe_join([leading_visual, content])
112
+ end
113
+ end
114
+ end
115
+
116
+ def render_button_to
117
+ form_class = options[:form_class] || 'contents'
118
+
119
+ if label.present?
120
+ button_to(helpers.safe_join([leading_visual, label]), url,
121
+ options.merge(form_class:))
122
+ else
123
+ button_to(url, options.merge(form_class:)) do
124
+ helpers.safe_join([leading_visual, content])
125
+ end
126
+ end
127
+ end
128
+
129
+ def call
130
+ if button_to_helper
131
+ raise(ArgumentError, 'URL is required') if url.blank?
132
+
133
+ @options = options.merge(disabled:) if disabled
134
+ render_button_to
135
+ elsif url.present?
136
+ if disabled
137
+ @options = options.merge(disabled:) if disabled
138
+ render_button
139
+ else
140
+ render_link
141
+ end
142
+ else
143
+ @options = options.merge(disabled:) if disabled
144
+ render_button
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tailwind_merge'
4
+
5
+ module Pulse
6
+ # This core `Component` class provides a place to include shared modules
7
+ # and helper methods that can be used by any child component.
8
+ class Component < ViewComponent::Base
9
+ TW_MERGE = TailwindMerge::Merger.new(config: { prefix: 'pulse-' })
10
+
11
+ include AttributesHelper
12
+ include FetchOrFallbackHelper
13
+ include SvgHelper
14
+
15
+ # include Pulse::ViewHelper
16
+
17
+ delegate :inline_svg_tag, :inline_svg, to: :helpers
18
+
19
+ # Merge nested attributes together. Particularly helpful when combining
20
+ # data attributes without squashing previously existing data.
21
+ #
22
+ # For example:
23
+ #
24
+ # ```ruby
25
+ # data = { data: { controller: "gallery" } }
26
+ #
27
+ # @options[:data] = merge_attributes(
28
+ # data,
29
+ # action: "click->gallery#next",
30
+ # )
31
+ # ```
32
+ #
33
+ # Returns:
34
+ #
35
+ # ```ruby
36
+ # @options[:data]
37
+ # # => { controller: "gallery", action: "click->gallery#next" }
38
+ # ```
39
+ #
40
+ def merge_attributes(*args)
41
+ args = Array.new(2) { {} } if args.compact.blank?
42
+ hashed_args = args.map { |el| el.presence || {} }
43
+
44
+ hashed_args.first.deep_merge(hashed_args.second) do |_key, val, other_val|
45
+ val + " #{other_val}"
46
+ end
47
+ end
48
+
49
+ def merge_classes(*args)
50
+ args = args.compact_blank
51
+ return '' if args.empty?
52
+
53
+ classes = class_names(*args)
54
+ return classes unless args.many?
55
+
56
+ # Handle arbitrary variants, which `class_names` will escape
57
+ classes = CGI.unescapeHTML(classes) if classes.include?('[&')
58
+
59
+ TW_MERGE.merge(classes)
60
+ end
61
+
62
+ def self.generate_id(base_name: name.demodulize.underscore.dasherize)
63
+ "#{base_name}-#{SecureRandom.uuid}"
64
+ end
65
+
66
+ private
67
+
68
+ def raise_on_invalid_options?
69
+ true
70
+ # Rails.application.config.primer_view_components.raise_on_invalid_options
71
+ end
72
+
73
+ def deny_single_argument(key, help_text, **arguments)
74
+ raise ArgumentError, "`#{key}` is an invalid argument. #{help_text}" \
75
+ if should_raise_error? && arguments.key?(key)
76
+
77
+ arguments.except!(key)
78
+ end
79
+
80
+ def deny_aria_key(key, help_text, **arguments)
81
+ raise ArgumentError, help_text if should_raise_aria_error? &&
82
+ aria(key, arguments)
83
+ end
84
+
85
+ def deny_tag_argument(**arguments)
86
+ deny_single_argument(:tag, 'This component has a fixed tag.', **arguments)
87
+ end
88
+
89
+ def should_raise_error?
90
+ !Rails.env.production? &&
91
+ raise_on_invalid_options? &&
92
+ !ENV['PULSE_WARNINGS_DISABLED']
93
+ end
94
+
95
+ def should_raise_aria_error?
96
+ !Rails.env.production?
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pulse
4
+ # Conditionally renders a `BaseComponent` around the given content. If the given condition
5
+ # is true, a `BaseComponent` will render around the content. If the condition is false, only
6
+ # the content is rendered.
7
+ class ConditionalWrapper < Pulse::Component
8
+ # @param condition [Boolean] Whether or not to wrap the content in a `BaseComponent`.
9
+ # @param base_component_arguments [Hash] The arguments to pass to `BaseComponent`.
10
+ def initialize(condition:, **base_component_arguments)
11
+ @condition = condition
12
+ @base_component_arguments = base_component_arguments
13
+ end
14
+
15
+ def call
16
+ return content unless @condition
17
+
18
+ BaseComponent.new(**@base_component_arguments).render_in(self) { content }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pulse
4
+ # Use `Content` as a helper to render content passed to a slot without adding
5
+ # any tags.
6
+ class Content < Pulse::Component
7
+ def call
8
+ content
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ /* Counter */
2
+ .Counter {
3
+ @apply pulse-inline-block pulse-min-w-[var(--base-size-20)] pulse-text-[length:var(--text-body-size-small)] pulse-font-[number:var(--base-text-weight-medium)] pulse-leading-[calc(var(--base-size-20)_-_var(--borderWidth-thin)_*_2)] pulse-text-[color:var(--fgColor-default)] pulse-text-center bg-[color:var(--bgColor-neutral-muted,var(--color-neutral-muted))] pulse-border-[length:var(--borderWidth-thin)] pulse-border-[color:var(--counter-borderColor)] pulse-px-1.5 pulse-py-0 pulse-rounded-[2em] pulse-border-solid empty:pulse-hidden; /* makes sure it's a circle with just one digit */ /* 20px - 2px for the borders */
4
+ }
5
+ .Counter--primary {
6
+ @apply pulse-text-[color:var(--pulse-fgColor-onEmphasis)] pulse-bg-[color:var(--bgColor-neutral-emphasis)];
7
+ }
8
+ .Counter--secondary {
9
+ @apply pulse-text-[color:var(--fgColor-muted)] pulse-bg-[color:var(--bgColor-neutral-muted)];
10
+ }
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pulse
4
+ # Use `Counter` to add a count to navigational elements and buttons.
5
+ #
6
+ # @accessibility
7
+ # Always use `Counter` with adjacent text that provides supplementary information regarding what the count is for. For instance, `Counter`
8
+ # should be accompanied with text such as `issues` or `pull requests`.
9
+ #
10
+ class Counter < Pulse::Component
11
+ DEFAULT_SCHEME = :default
12
+ SCHEME_MAPPINGS = {
13
+ DEFAULT_SCHEME => '',
14
+ :primary => 'Counter--primary',
15
+ :secondary => 'Counter--secondary',
16
+ # deprecated
17
+ :gray => 'Counter--primary',
18
+ :light_gray => 'Counter--secondary'
19
+ }.freeze
20
+ DEPRECATED_SCHEME_OPTIONS = [:gray, :light_gray].freeze
21
+ SCHEME_OPTIONS = (SCHEME_MAPPINGS.keys - DEPRECATED_SCHEME_OPTIONS).freeze
22
+
23
+ # @param count [Integer, Float::INFINITY, nil] The number to be displayed (e.x. # of issues, pull requests)
24
+ # @param scheme [Symbol] Color scheme. <%= one_of(Pulse::Counter::SCHEME_OPTIONS) %>
25
+ # @param limit [Integer, nil] Maximum value to display. Pass `nil` for no limit. (e.x. if `count` == 6,000 and `limit` == 5000, counter will display "5,000+")
26
+ # @param hide_if_zero [Boolean] If true, a `hidden` attribute is added to the counter if `count` is zero.
27
+ # @param text [String] Text to display instead of count.
28
+ # @param round [Boolean] Whether to apply our standard rounding logic to value.
29
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
30
+ def initialize(
31
+ count: 0,
32
+ scheme: DEFAULT_SCHEME,
33
+ limit: 5_000,
34
+ hide_if_zero: false,
35
+ text: '',
36
+ round: false,
37
+ **system_arguments
38
+ )
39
+ @count = count
40
+ @limit = limit
41
+ @hide_if_zero = hide_if_zero
42
+ @text = text
43
+ @round = round
44
+ @system_arguments = deny_tag_argument(**system_arguments)
45
+
46
+ @has_limit = !@limit.nil?
47
+ @system_arguments[:title] = title
48
+ @system_arguments[:tag] = :span
49
+ @system_arguments[:classes] = merge_classes(
50
+ 'Counter',
51
+ @system_arguments[:classes],
52
+ SCHEME_MAPPINGS[fetch_or_fallback(
53
+ SCHEME_OPTIONS,
54
+ scheme,
55
+ DEFAULT_SCHEME
56
+ )]
57
+ )
58
+ @system_arguments[:hidden] = true if count.zero? && hide_if_zero
59
+ end
60
+
61
+ def call
62
+ render(Pulse::BaseComponent.new(**@system_arguments)) { value }
63
+ end
64
+
65
+ private
66
+
67
+ def title
68
+ if @text.present?
69
+ @text
70
+ elsif @count.nil?
71
+ 'Not available'
72
+ elsif @count == Float::INFINITY
73
+ 'Infinity'
74
+ else
75
+ count = @count.to_i
76
+ str = number_with_delimiter(@has_limit ? [count, @limit].min : count)
77
+ str += '+' if @has_limit && count > @limit
78
+ str
79
+ end
80
+ end
81
+
82
+ def value
83
+ if @text.present?
84
+ @text
85
+ elsif @count.nil?
86
+ '' # CSS will hide it
87
+ elsif @count == Float::INFINITY
88
+ '∞'
89
+ else
90
+ if @round
91
+ count = @has_limit ? [@count.to_i, @limit].min : @count.to_i
92
+ precision = count.between?(100_000, 999_999) ? 0 : 1
93
+ units = { thousand: 'k', million: 'm', billion: 'b' }
94
+ str = number_to_human(count, precision:, significant: false, units:,
95
+ format: '%n%u')
96
+ else
97
+ @count = @count.to_i
98
+ str = number_with_delimiter(@has_limit ? [@count,
99
+ @limit].min : @count)
100
+ end
101
+
102
+ str += '+' if @has_limit && @count.to_i > @limit
103
+ str
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,9 @@
1
+ = render Pulse::BaseComponent.new(**@system_arguments) do
2
+ - if @icon
3
+ = render Pulse::Icon.new(icon: @icon)
4
+ = content
5
+
6
+ - if @dismissible
7
+ = render Pulse::IconButton.new(icon: :x, scheme: :invisible, classes: 'flash-close js-flash-close', 'aria-label': 'Dismiss alert')
8
+
9
+ = action
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pulse
4
+ # Use `Flash` to inform users of successful or pending actions.
5
+ class Flash < Pulse::Component
6
+ # Optional action content showed on the right side of the component.
7
+ #
8
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
9
+ renders_one :action, lambda { |**system_arguments|
10
+ deny_tag_argument(**system_arguments)
11
+ system_arguments[:tag] = :div
12
+ system_arguments[:classes] = class_names(
13
+ system_arguments[:classes],
14
+ 'flash-action'
15
+ )
16
+
17
+ Pulse::BaseComponent.new(**system_arguments)
18
+ }
19
+
20
+ # TODO: Style icon in scheme color
21
+ DEFAULT_SCHEME = :default
22
+ SCHEME_MAPPINGS = {
23
+ default: 'pulse-bg-[#ddf4ff] pulse-border pulse-border-[#54aeff]',
24
+ warning: 'pulse-bg-[#fff8c5] pulse-border pulse-border-[#f6e05e]',
25
+ danger: 'pulse-bg-[#ffebe9] pulse-border pulse-border-[#ff8182]',
26
+ success: 'pulse-bg-[#dafbe1] pulse-border pulse-border-[#4ac26b]'
27
+ }.freeze
28
+
29
+ # @param full [Boolean] Whether the component should take up the full width of the screen.
30
+ # @param spacious [Boolean] Whether to add margin to the bottom of the component.
31
+ # @param dismissible [Boolean] Whether the component can be dismissed with an X button.
32
+ # @param icon [Symbol] Name of Octicon icon to use.
33
+ # @param scheme [Symbol] <%= one_of(Pulse::Flash::SCHEME_MAPPINGS.keys) %>
34
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
35
+ def initialize(full: false, spacious: false, dismissible: false,
36
+ icon: nil, scheme: DEFAULT_SCHEME, **system_arguments)
37
+ @icon = icon
38
+ @dismissible = dismissible
39
+ @system_arguments = deny_tag_argument(**system_arguments)
40
+ @system_arguments[:tag] = :div
41
+ @system_arguments[:classes] = class_names(
42
+ 'pulse-px-3 pulse-py-2 pulse-font-medium pulse-rounded-lg pulse-my-3',
43
+ 'pulse-flex pulse-gap-2 pulse-items-center pulse-justify-start',
44
+ SCHEME_MAPPINGS[fetch_or_fallback(SCHEME_MAPPINGS.keys, scheme,
45
+ DEFAULT_SCHEME)],
46
+ system_arguments[:classes],
47
+ 'flash-full': full
48
+ )
49
+ @system_arguments[:mb] ||= spacious ? 4 : nil
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,8 @@
1
+ /! components/pulse/forms/error_explanation
2
+ = render Pulse::BaseComponent.new(tag: :div, **@options) do
3
+ = render Pulse::Text.new(type: :body1, tag: :h2) do
4
+ = t('errors.template.header', count: resource.errors.count, model: resource.class.model_name.human.downcase)
5
+
6
+ ul class="pulse-list-disc pulse-list-inside"
7
+ - errors.full_messages.each do |msg|
8
+ li = msg
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pulse
4
+ module Forms
5
+ # Render error message box for an ActiveModel resource
6
+ class ErrorExplanation < ViewComponent::Base
7
+ def initialize(resource:, **options)
8
+ @resource = resource
9
+ @options = options
10
+ @options[:classes] = class_names(
11
+ 'pulse-bg-error-50 pulse-text-error pulse-px-3 pulse-py-2',
12
+ 'pulse-font-medium pulse-rounded-lg pulse-my-3',
13
+ options[:classes]
14
+ )
15
+ end
16
+
17
+ def errors
18
+ @errors = ActiveModel::Errors.new(resource)
19
+ # Only display the last of each attribute error, to avoid needless
20
+ # duplication.
21
+ resource.errors.group_by(&:attribute).values.map(&:last).each do |error|
22
+ @errors.add(error.attribute, error.message)
23
+ end
24
+ @errors
25
+ end
26
+
27
+ def render?
28
+ resource.errors.any?
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :resource
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pulse
4
+ # Render icons from the images/pulse_view_components/icons directory.
5
+ class Icon < Pulse::Component
6
+ SIZE_DEFAULT = :sm
7
+ SIZE_MAPPINGS = {
8
+ xs: 12,
9
+ sm: 16,
10
+ md: 24
11
+ }.freeze
12
+ SIZE_OPTIONS = SIZE_MAPPINGS.keys
13
+
14
+ def initialize(icon: nil, size: SIZE_DEFAULT, **system_arguments)
15
+ @icon = "pulse_view_components/icons/#{icon}"
16
+
17
+ @system_arguments = system_arguments
18
+ @system_arguments[:class] = class_names(
19
+ 'pulse-icon',
20
+ system_arguments[:classes]
21
+ )
22
+ @system_arguments[:height] ||=
23
+ SIZE_MAPPINGS[fetch_or_fallback(SIZE_OPTIONS, size, SIZE_DEFAULT)]
24
+ end
25
+
26
+ def call
27
+ render_svg(@icon, **@system_arguments)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ /! components/pulse/label
2
+ = render Pulse::BaseComponent.new(**@options,
3
+ title: "Label: #{title}",
4
+ style: "background-color: ##{bg_color}") do
5
+ = title
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pulse
4
+ # A kind of badge. Used for displaying github labels. Perhaps could be
5
+ # consolidated with badges?
6
+ # Accepts Hex color value for bg_color, with or without a leading "#"
7
+ class Label < Pulse::Component
8
+ attr_reader :title, :bg_color
9
+
10
+ def initialize(title:, bg_color: 'ffffff', **options)
11
+ @title = title
12
+ @bg_color = bg_color || 'ffffff'
13
+ @options = options
14
+ @options[:classes] = merge_classes(
15
+ 'pulse-px-2 pulse-inline-flex pulse-text-xs print:pulse-text-2xs',
16
+ 'pulse-leading-5 pulse-font-semibold pulse-rounded-full',
17
+ text_color,
18
+ options[:classes]
19
+ )
20
+ @options[:tag] ||= :span
21
+ end
22
+
23
+ private
24
+
25
+ def text_color
26
+ @bg_color = @bg_color[1..] if @bg_color[0] == '#'
27
+
28
+ r, g, b = @bg_color.scan(/../).map { |color| color.to_i(16) }
29
+
30
+ # Normalize RGB values to 0-1
31
+ r_norm = r / 255.0
32
+ g_norm = g / 255.0
33
+ b_norm = b / 255.0
34
+
35
+ # Calculate luminance
36
+ luminance = (0.2126 * r_norm) + (0.7152 * g_norm) + (0.0722 * b_norm)
37
+
38
+ # Choose text color based on luminance
39
+ luminance > 0.5 ? 'pulse-text-black' : 'pulse-text-white'
40
+ end
41
+ end
42
+ end