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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +88 -0
- data/LICENSE.txt +22 -0
- data/README.md +78 -0
- data/app/assets/builds/js/pulse_view_components.js +9543 -0
- data/app/assets/builds/js/pulse_view_components.js.map +7 -0
- data/app/assets/builds/pulse_view_components.css +1 -0
- data/app/assets/config/manifest.js +2 -0
- data/app/assets/images/pulse_view_components/icons/alert.svg +3 -0
- data/app/assets/images/pulse_view_components/icons/archive.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/arrow-both.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/arrow-down.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/arrow-left.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/arrow-right.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/arrow-switch.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/arrow-up.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/beaker.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/bell-slash.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/bell.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/bookmark-slash.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/bookmark.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/calendar.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/check-circle.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/check.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/chevron-down.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/circle.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/clock.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/close.svg +9 -0
- data/app/assets/images/pulse_view_components/icons/cloud.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/comment-discussion.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/comment.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/copy.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/database.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/dot.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/dots-outline.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/download.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/duplicate.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/eye.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/file-added.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/file-directory.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/file-media.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/file.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/filter.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/flame.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/gear.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/gift.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/globe.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/goal.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/graph.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/heart-fill.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/heart.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/history.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/home.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/hourglass.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/image.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/inbox.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/info.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/issue.svg +3 -0
- data/app/assets/images/pulse_view_components/icons/key.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/law.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/link-external.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/link.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/location.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/log.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/mail.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/mark-github.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/mark-jira.svg +3 -0
- data/app/assets/images/pulse_view_components/icons/megaphone.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/no-entry.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/note.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/organization.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/package.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/paintbrush.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/paperclip.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/paste.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/pencil.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/people.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/person-add.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/person.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/pin-slash.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/pin.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/play.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/plus-circle.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/plus.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/project.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/pulse.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/question.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/redo.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/reply.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/report.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/rss.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/ruby.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/search.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/share.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/shield.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/sliders.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/smiley.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/sort.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/sort_asc.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/sort_desc.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/stack.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/star-fill.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/star.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/stopwatch.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/sun.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/sync.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/table.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/tag.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/tasklist.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/telescope.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/three-bars.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/thumbsdown.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/thumbsup.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/tools.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/trash.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/trophy.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/undo.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/unlink.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/upload.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/x-circle.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/x.svg +1 -0
- data/app/assets/images/pulse_view_components/icons/zap.svg +1 -0
- data/app/assets/javascripts/pulse_view_components.js +0 -0
- data/app/assets/stylesheets/pulse_view_components/application.tailwind.css +10 -0
- data/app/components/pulse/action_list/divider.rb +35 -0
- data/app/components/pulse/action_list/form_wrapper.html.slim +9 -0
- data/app/components/pulse/action_list/form_wrapper.rb +63 -0
- data/app/components/pulse/action_list/heading.rb +52 -0
- data/app/components/pulse/action_list/item.html.slim +43 -0
- data/app/components/pulse/action_list/item.rb +319 -0
- data/app/components/pulse/action_list.css +568 -0
- data/app/components/pulse/action_list.rb +261 -0
- data/app/components/pulse/badge.rb +59 -0
- data/app/components/pulse/base_component.rb +36 -0
- data/app/components/pulse/blank_slate.html.slim +10 -0
- data/app/components/pulse/blank_slate.rb +19 -0
- data/app/components/pulse/blank_slate.yml +2 -0
- data/app/components/pulse/border_box/header.html.slim +4 -0
- data/app/components/pulse/border_box/header.rb +48 -0
- data/app/components/pulse/border_box.html.slim +10 -0
- data/app/components/pulse/border_box.rb +124 -0
- data/app/components/pulse/box.rb +17 -0
- data/app/components/pulse/breadcrumb_item.rb +40 -0
- data/app/components/pulse/breadcrumbs.html.slim +9 -0
- data/app/components/pulse/breadcrumbs.rb +17 -0
- data/app/components/pulse/button.rb +148 -0
- data/app/components/pulse/component.rb +99 -0
- data/app/components/pulse/conditional_wrapper.rb +21 -0
- data/app/components/pulse/content.rb +11 -0
- data/app/components/pulse/counter.css +10 -0
- data/app/components/pulse/counter.rb +107 -0
- data/app/components/pulse/flash.html.slim +9 -0
- data/app/components/pulse/flash.rb +52 -0
- data/app/components/pulse/forms/error_explanation.html.slim +8 -0
- data/app/components/pulse/forms/error_explanation.rb +36 -0
- data/app/components/pulse/icon.rb +30 -0
- data/app/components/pulse/label.html.slim +5 -0
- data/app/components/pulse/label.rb +42 -0
- data/app/components/pulse/layout.css +239 -0
- data/app/components/pulse/layout.html.slim +7 -0
- data/app/components/pulse/layout.rb +141 -0
- data/app/components/pulse/link.html.slim +7 -0
- data/app/components/pulse/link.rb +52 -0
- data/app/components/pulse/modal.html.slim +51 -0
- data/app/components/pulse/modal.rb +64 -0
- data/app/components/pulse/modal_controller.js +77 -0
- data/app/components/pulse/nav_link.html.slim +16 -0
- data/app/components/pulse/nav_link.rb +25 -0
- data/app/components/pulse/nav_list/divider.rb +13 -0
- data/app/components/pulse/nav_list/group.html.slim +6 -0
- data/app/components/pulse/nav_list/group.rb +110 -0
- data/app/components/pulse/nav_list/heading.rb +36 -0
- data/app/components/pulse/nav_list/item.rb +171 -0
- data/app/components/pulse/nav_list.html.slim +11 -0
- data/app/components/pulse/nav_list.rb +225 -0
- data/app/components/pulse/popover.html.slim +5 -0
- data/app/components/pulse/popover.rb +88 -0
- data/app/components/pulse/progress_bar.html.slim +5 -0
- data/app/components/pulse/progress_bar.rb +23 -0
- data/app/components/pulse/progress_bar.yml +2 -0
- data/app/components/pulse/table/cell.html.slim +10 -0
- data/app/components/pulse/table/cell.rb +90 -0
- data/app/components/pulse/table/cell.yml +5 -0
- data/app/components/pulse/table/column.rb +22 -0
- data/app/components/pulse/table.html.slim +50 -0
- data/app/components/pulse/table.rb +111 -0
- data/app/components/pulse/table.yml +2 -0
- data/app/components/pulse/text.rb +72 -0
- data/app/components/pulse/toast.html.slim +14 -0
- data/app/components/pulse/toast.rb +85 -0
- data/app/components/pulse/toast_controller.js +32 -0
- data/app/javascript/pulse_view_components/application.js +24 -0
- data/app/javascript/pulse_view_components.js +4 -0
- data/app/lib/pulse/attributes_helper.rb +107 -0
- data/app/lib/pulse/fetch_or_fallback_helper.rb +53 -0
- data/app/lib/pulse/svg_helper.rb +28 -0
- data/lib/pulse/view_components/engine.rb +74 -0
- data/lib/pulse/view_components/version.rb +17 -0
- data/lib/pulse/view_components.rb +14 -0
- data/lib/pulse_view_components.rb +3 -0
- data/lib/tasks/pulse_view_components_tasks.rake +8 -0
- data/previews/pulse/badge_preview.rb +51 -0
- data/previews/pulse/blank_slate_preview.rb +14 -0
- data/previews/pulse/border_box_preview.rb +48 -0
- data/previews/pulse/breadcrumbs_preview.rb +13 -0
- data/previews/pulse/button_preview.rb +137 -0
- data/previews/pulse/counter_preview.rb +14 -0
- data/previews/pulse/flash_preview.rb +85 -0
- data/previews/pulse/icon_preview.rb +12 -0
- data/previews/pulse/label_preview.rb +10 -0
- data/previews/pulse/layout_preview.rb +34 -0
- data/previews/pulse/link_preview.rb +55 -0
- data/previews/pulse/modal_preview.rb +48 -0
- data/previews/pulse/nav_link_preview.rb +48 -0
- data/previews/pulse/nav_list_preview.rb +138 -0
- data/previews/pulse/popover_preview.rb +77 -0
- data/previews/pulse/progress_bar_preview.rb +23 -0
- data/previews/pulse/table_preview.rb +168 -0
- data/previews/pulse/text_preview.rb +102 -0
- data/previews/pulse/toast_preview.rb +67 -0
- 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,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,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
|