primer_view_components 0.0.1 → 0.0.7

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +92 -2
  3. data/README.md +149 -0
  4. data/app/components/primer/avatar_component.rb +28 -0
  5. data/app/components/primer/base_component.rb +42 -0
  6. data/app/components/primer/blankslate_component.html.erb +27 -0
  7. data/app/components/primer/blankslate_component.rb +164 -0
  8. data/app/components/primer/border_box_component.html.erb +26 -0
  9. data/app/components/primer/border_box_component.rb +77 -0
  10. data/app/components/primer/box_component.rb +14 -0
  11. data/app/components/primer/breadcrumb_component.html.erb +8 -0
  12. data/app/components/primer/breadcrumb_component.rb +52 -0
  13. data/app/components/primer/button_component.rb +58 -0
  14. data/app/components/primer/component.rb +9 -0
  15. data/app/components/primer/counter_component.rb +85 -0
  16. data/app/components/primer/details_component.html.erb +8 -0
  17. data/app/components/primer/details_component.rb +63 -0
  18. data/app/components/primer/dropdown_menu_component.html.erb +8 -0
  19. data/app/components/primer/dropdown_menu_component.rb +28 -0
  20. data/app/components/primer/flex_component.rb +81 -0
  21. data/app/components/primer/flex_item_component.rb +21 -0
  22. data/app/components/primer/heading_component.rb +14 -0
  23. data/app/components/primer/label_component.rb +48 -0
  24. data/app/components/primer/layout_component.html.erb +17 -0
  25. data/app/components/primer/layout_component.rb +28 -0
  26. data/app/components/primer/link_component.rb +19 -0
  27. data/app/components/primer/popover_component.html.erb +10 -0
  28. data/app/components/primer/popover_component.rb +75 -0
  29. data/app/components/primer/progress_bar_component.html.erb +5 -0
  30. data/app/components/primer/progress_bar_component.rb +66 -0
  31. data/app/components/primer/slot.rb +8 -0
  32. data/app/components/primer/state_component.rb +53 -0
  33. data/app/components/primer/subhead_component.html.erb +17 -0
  34. data/app/components/primer/subhead_component.rb +89 -0
  35. data/app/components/primer/text_component.rb +14 -0
  36. data/app/components/primer/timeline_item_component.html.erb +17 -0
  37. data/app/components/primer/timeline_item_component.rb +69 -0
  38. data/app/components/primer/underline_nav_component.html.erb +11 -0
  39. data/app/components/primer/underline_nav_component.rb +22 -0
  40. data/app/components/primer/view_components.rb +48 -0
  41. data/lib/primer/class_name_helper.rb +27 -0
  42. data/lib/primer/classify.rb +245 -0
  43. data/lib/primer/fetch_or_fallback_helper.rb +41 -0
  44. data/lib/primer/view_components.rb +3 -0
  45. data/lib/primer/view_components/engine.rb +11 -0
  46. data/lib/primer/view_components/version.rb +1 -1
  47. metadata +203 -4
@@ -0,0 +1,17 @@
1
+ <%= render Primer::BaseComponent.new(**@kwargs) do %>
2
+ <% if heading.present? %>
3
+ <%= render Primer::BaseComponent.new(**heading.kwargs) do %>
4
+ <%= heading.content %>
5
+ <% end %>
6
+ <% end %>
7
+ <% if actions.present? %>
8
+ <%= render Primer::BaseComponent.new(**actions.kwargs) do %>
9
+ <%= actions.content %>
10
+ <% end %>
11
+ <% end %>
12
+ <% if description.present? %>
13
+ <%= render Primer::BaseComponent.new(**description.kwargs) do %>
14
+ <%= description.content %>
15
+ <% end %>
16
+ <% end %>
17
+ <% end %>
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This component consists of a .Subhead container, which has a light gray bottom border.
4
+
5
+ # Use a heading element whenever possible as they can be
6
+ # used as navigation for assistive technologies, and avoid skipping levels.
7
+
8
+ # ## Basic example
9
+
10
+ # The `Primer::SubheadComponent` can take the following arguments:
11
+
12
+ # 1. `heading` (string). The heading to be rendered.
13
+ # 2. `actions` (content). Slot to render any actions to the right of heading.
14
+ # 3. `description` (string). Slot to render description under the heading.
15
+
16
+ # ```erb
17
+ # <%= Primer::SubheadComponent.new(heading: "Hello world")) do |component| %>
18
+ # <% component.slot(:actions) do %>
19
+ # My Actions
20
+ # <% end %>
21
+ # <% end %>
22
+ # ```
23
+ module Primer
24
+ class SubheadComponent < Primer::Component
25
+ include ViewComponent::Slotable
26
+
27
+ with_slot :heading, class_name: "Heading"
28
+ with_slot :actions, class_name: "Actions"
29
+ with_slot :description, class_name: "Description"
30
+
31
+ def initialize(spacious: false, hide_border: false, **kwargs)
32
+ @kwargs = kwargs
33
+
34
+ @kwargs[:tag] = :div
35
+ @kwargs[:classes] =
36
+ class_names(
37
+ @kwargs[:classes],
38
+ "Subhead hx_Subhead--responsive",
39
+ "Subhead--spacious": spacious,
40
+ "border-bottom-0": hide_border
41
+ )
42
+ @kwargs[:mb] ||= hide_border ? 0 : nil
43
+ end
44
+
45
+ def render?
46
+ heading.present?
47
+ end
48
+
49
+ class Heading < ViewComponent::Slot
50
+ include ClassNameHelper
51
+
52
+ attr_reader :kwargs
53
+
54
+ def initialize(danger: false, **kwargs)
55
+ @kwargs = kwargs
56
+ @kwargs[:tag] ||= :div
57
+ @kwargs[:classes] = class_names(
58
+ @kwargs[:classes],
59
+ "Subhead-heading",
60
+ "Subhead-heading--danger": danger
61
+ )
62
+ end
63
+ end
64
+
65
+ class Actions < ViewComponent::Slot
66
+ include ClassNameHelper
67
+
68
+ attr_reader :kwargs
69
+
70
+ def initialize(**kwargs)
71
+ @kwargs = kwargs
72
+ @kwargs[:tag] = :div
73
+ @kwargs[:classes] = class_names(@kwargs[:classes], "Subhead-actions")
74
+ end
75
+ end
76
+
77
+ class Description < ViewComponent::Slot
78
+ include ClassNameHelper
79
+
80
+ attr_reader :kwargs
81
+
82
+ def initialize(**kwargs)
83
+ @kwargs = kwargs
84
+ @kwargs[:tag] = :div
85
+ @kwargs[:classes] = class_names(@kwargs[:classes], "Subhead-description")
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ class TextComponent < Primer::Component
5
+ def initialize(**kwargs)
6
+ @kwargs = kwargs
7
+ @kwargs[:tag] ||= :span
8
+ end
9
+
10
+ def call
11
+ render(Primer::BaseComponent.new(**@kwargs)) { content }
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ <%= render Primer::BaseComponent.new(**kwargs) do %>
2
+ <% if avatar %>
3
+ <%= render Primer::AvatarComponent.new(alt: avatar.alt, src: avatar.src, size: avatar.size, square: avatar.square, **avatar.kwargs) %>
4
+ <% end %>
5
+
6
+ <% if badge %>
7
+ <%= render Primer::BaseComponent.new(**badge.kwargs) do %>
8
+ <%= octicon badge.icon %>
9
+ <% end %>
10
+ <% end %>
11
+
12
+ <% if body %>
13
+ <%= render Primer::BaseComponent.new(**body.kwargs) do %>
14
+ <%= body.content %>
15
+ <% end %>
16
+ <% end %>
17
+ <% end %>
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ class TimelineItemComponent < Primer::Component
5
+ include ViewComponent::Slotable
6
+
7
+ with_slot :avatar, class_name: "Avatar"
8
+ with_slot :badge, class_name: "Badge"
9
+ with_slot :body, class_name: "Body"
10
+
11
+ attr_reader :kwargs
12
+ def initialize(condensed: false, **kwargs)
13
+ @kwargs = kwargs
14
+ @kwargs[:tag] = :div
15
+ @kwargs[:classes] = class_names(
16
+ "TimelineItem",
17
+ condensed ? "TimelineItem--condensed" : "",
18
+ kwargs[:classes]
19
+ )
20
+ end
21
+
22
+ def render?
23
+ avatar.present? || badge.present? || body.present?
24
+ end
25
+
26
+ class Avatar < Primer::Slot
27
+ attr_reader :kwargs, :alt, :src, :size, :square
28
+ def initialize(alt: nil, src: nil, size: 40, square: true, **kwargs)
29
+ @alt = alt
30
+ @src = src
31
+ @size = size
32
+ @square = square
33
+
34
+ @kwargs = kwargs
35
+ @kwargs[:tag] = :div
36
+ @kwargs[:classes] = class_names(
37
+ "TimelineItem-avatar",
38
+ kwargs[:classes]
39
+ )
40
+ end
41
+ end
42
+
43
+ class Badge < Primer::Slot
44
+ attr_reader :kwargs, :icon
45
+ def initialize(icon: nil, **kwargs)
46
+ @icon = icon
47
+
48
+ @kwargs = kwargs
49
+ @kwargs[:tag] = :div
50
+ @kwargs[:classes] = class_names(
51
+ "TimelineItem-badge",
52
+ kwargs[:classes]
53
+ )
54
+ end
55
+ end
56
+
57
+ class Body < Primer::Slot
58
+ attr_reader :kwargs
59
+ def initialize(**kwargs)
60
+ @kwargs = kwargs
61
+ @kwargs[:tag] = :div
62
+ @kwargs[:classes] = class_names(
63
+ "TimelineItem-body",
64
+ kwargs[:classes]
65
+ )
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,11 @@
1
+ <%= render Primer::BaseComponent.new(**@kwargs) do %>
2
+ <% if actions && @align == :right %>
3
+ <%= actions %>
4
+ <% end %>
5
+ <%= render Primer::BaseComponent.new(tag: :ul, classes: "UnderlineNav-body list-style-none") do %>
6
+ <%= body %>
7
+ <% end %>
8
+ <% if actions && @align == :left %>
9
+ <%= actions %>
10
+ <% end %>
11
+ <% end %>
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ class UnderlineNavComponent < Primer::Component
5
+ ALIGN_DEFAULT = :left
6
+ ALIGN_OPTIONS = [ALIGN_DEFAULT, :right]
7
+
8
+ with_content_areas :body, :actions
9
+
10
+ def initialize(align: ALIGN_DEFAULT, **kwargs)
11
+ @align = fetch_or_fallback(ALIGN_OPTIONS, align, ALIGN_DEFAULT)
12
+
13
+ @kwargs = kwargs
14
+ @kwargs[:tag] = :nav
15
+ @kwargs[:classes] = class_names(
16
+ @kwargs[:classes],
17
+ "UnderlineNav",
18
+ "UnderlineNav--right" => @align == :right
19
+ )
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext"
4
+
5
+ # ViewComponent
6
+
7
+ require "view_component/engine"
8
+
9
+ # Octicons
10
+
11
+ require "octicons_helper/helper"
12
+
13
+ # Helpers
14
+
15
+ require "primer/class_name_helper"
16
+ require "primer/classify"
17
+ require "primer/fetch_or_fallback_helper"
18
+
19
+ # Base configurations
20
+
21
+ require_relative "component"
22
+ require_relative "base_component"
23
+ require_relative "slot"
24
+
25
+ # Components
26
+
27
+ require_relative "avatar_component"
28
+ require_relative "blankslate_component"
29
+ require_relative "border_box_component"
30
+ require_relative "box_component"
31
+ require_relative "breadcrumb_component"
32
+ require_relative "button_component"
33
+ require_relative "counter_component"
34
+ require_relative "details_component"
35
+ require_relative "dropdown_menu_component"
36
+ require_relative "flex_component"
37
+ require_relative "flex_item_component"
38
+ require_relative "heading_component"
39
+ require_relative "label_component"
40
+ require_relative "layout_component"
41
+ require_relative "link_component"
42
+ require_relative "popover_component"
43
+ require_relative "progress_bar_component"
44
+ require_relative "state_component"
45
+ require_relative "subhead_component"
46
+ require_relative "text_component"
47
+ require_relative "timeline_item_component"
48
+ require_relative "underline_nav_component"
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Inspired by https://github.com/JedWatson/classnames
4
+ #
5
+ # Helps build a list of conditional class names
6
+ module Primer
7
+ module ClassNameHelper
8
+ def class_names(*args)
9
+ classes = []
10
+
11
+ args.each do |class_name|
12
+ case class_name
13
+ when String
14
+ classes << class_name if class_name.present?
15
+ when Hash
16
+ class_name.each do |key, val|
17
+ classes << key if val
18
+ end
19
+ when Array
20
+ classes << class_names(*class_name).presence
21
+ end
22
+ end
23
+
24
+ classes.compact.uniq.join(" ")
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ class Classify
5
+ MARGIN_DIRECTION_KEYS = [:mt, :ml, :mb, :mr]
6
+ SPACING_KEYS = ([:m, :my, :mx, :p, :py, :px, :pt, :pl, :pb, :pr] + MARGIN_DIRECTION_KEYS).freeze
7
+ DIRECTION_KEY = :direction
8
+ JUSTIFY_CONTENT_KEY = :justify_content
9
+ ALIGN_ITEMS_KEY = :align_items
10
+ DISPLAY_KEY = :display
11
+ RESPONSIVE_KEYS = ([DISPLAY_KEY, DIRECTION_KEY, JUSTIFY_CONTENT_KEY, ALIGN_ITEMS_KEY, :col, :float] + SPACING_KEYS).freeze
12
+ BREAKPOINTS = ["", "-sm", "-md", "-lg"]
13
+
14
+ # Keys where we can simply translate { key: value } into ".key-value"
15
+ CONCAT_KEYS = SPACING_KEYS + [:hide, :position, :v, :float, :col, :text, :box_shadow].freeze
16
+
17
+ INVALID_CLASS_NAME_PREFIXES =
18
+ (["bg-", "color-", "text-", "d-", "v-align-", "wb-", "text-", "box-shadow-"] + CONCAT_KEYS.map { |k| "#{k}-" }).freeze
19
+
20
+ COLOR_KEY = :color
21
+ BG_KEY = :bg
22
+ VERTICAL_ALIGN_KEY = :vertical_align
23
+ WORD_BREAK_KEY = :word_break
24
+ TEXT_KEYS = [:text_align, :font_weight]
25
+ FLEX_KEY = :flex
26
+ FLEX_GROW_KEY = :flex_grow
27
+ FLEX_SHRINK_KEY = :flex_shrink
28
+ ALIGN_SELF_KEY = :align_self
29
+ WIDTH_KEY = :width
30
+ HEIGHT_KEY = :height
31
+ BOX_SHADOW_KEY = :box_shadow
32
+
33
+
34
+ BOOLEAN_MAPPINGS = {
35
+ underline: {
36
+ mappings: [
37
+ {
38
+ value: true,
39
+ css_class: "text-underline",
40
+ },
41
+ {
42
+ value: false,
43
+ css_class: "no-underline",
44
+ },
45
+ ],
46
+ },
47
+ top: {
48
+ mappings: [
49
+ {
50
+ value: false,
51
+ css_class: "top-0"
52
+ }
53
+ ]
54
+ },
55
+ bottom: {
56
+ mappings: [
57
+ {
58
+ value: false,
59
+ css_class: "bottom-0"
60
+ }
61
+ ]
62
+ },
63
+ left: {
64
+ mappings: [
65
+ {
66
+ value: false,
67
+ css_class: "left-0"
68
+ }
69
+ ]
70
+ },
71
+ right: {
72
+ mappings: [
73
+ {
74
+ value: false,
75
+ css_class: "right-0"
76
+ }
77
+ ]
78
+ }
79
+ }.freeze
80
+ BORDER_KEYS = [:border, :border_color].freeze
81
+ TYPOGRAPHY_KEYS = [:font_size].freeze
82
+ VALID_KEYS = (
83
+ CONCAT_KEYS +
84
+ BOOLEAN_MAPPINGS.keys +
85
+ BORDER_KEYS +
86
+ TYPOGRAPHY_KEYS +
87
+ TEXT_KEYS +
88
+ [
89
+ COLOR_KEY,
90
+ BG_KEY,
91
+ DISPLAY_KEY,
92
+ VERTICAL_ALIGN_KEY,
93
+ WORD_BREAK_KEY,
94
+ DIRECTION_KEY,
95
+ JUSTIFY_CONTENT_KEY,
96
+ ALIGN_ITEMS_KEY,
97
+ FLEX_KEY,
98
+ FLEX_GROW_KEY,
99
+ FLEX_SHRINK_KEY,
100
+ ALIGN_SELF_KEY,
101
+ WIDTH_KEY,
102
+ HEIGHT_KEY,
103
+ BOX_SHADOW_KEY
104
+ ]
105
+ ).freeze
106
+
107
+ class << self
108
+ def call(classes: "", style: nil, **args)
109
+ extracted_results = extract_hash(args)
110
+
111
+ {
112
+ class: [validated_class_names(classes), extracted_results[:classes]].compact.join(" ").presence,
113
+ style: [extracted_results[:styles], style].compact.join("").presence,
114
+ }.merge(extracted_results.except(:classes, :styles))
115
+ end
116
+
117
+ private
118
+
119
+ def validated_class_names(classes)
120
+ return unless classes.present?
121
+
122
+ if ENV["RAILS_ENV"] == "development"
123
+ invalid_class_names =
124
+ classes.split(" ").each_with_object([]) do |class_name, memo|
125
+ if INVALID_CLASS_NAME_PREFIXES.any? { |prefix| class_name.start_with?(prefix) }
126
+ memo << class_name
127
+ end
128
+ end
129
+
130
+ if invalid_class_names.any?
131
+ raise ArgumentError.new(
132
+ "Primer CSS class #{'name'.pluralize(invalid_class_names.length)} \
133
+ #{invalid_class_names.to_sentence} #{'is'.pluralize(invalid_class_names.length)} \
134
+ not allowed, use style arguments instead (https://github.com/primer/view_components#built-in-styling-arguments). This warning will not be raised in production.",
135
+ )
136
+ end
137
+ end
138
+
139
+ classes
140
+ end
141
+
142
+ # NOTE: This is a fairly naive implementation that we're building as we go.
143
+ # Feel free to refactor as this is thoroughly tested.
144
+ #
145
+ # Utility for mapping component configuration into Primer CSS class names
146
+ #
147
+ # styles_hash - A hash with utility keys that mimic the interface used by https://github.com/primer/components
148
+ #
149
+ # Returns a string of Primer CSS class names to be added to an HTML class attribute
150
+ #
151
+ # Example usage:
152
+ # extract_hash({ mt: 4, py: 2 }) => "mt-4 py-2"
153
+ def extract_hash(styles_hash)
154
+ out = styles_hash.each_with_object({ classes: [], styles: [] }) do |(key, value), memo|
155
+ next unless VALID_KEYS.include?(key)
156
+
157
+ if value.is_a?(Array) && !RESPONSIVE_KEYS.include?(key)
158
+ raise ArgumentError, "#{key} does not support responsive values"
159
+ end
160
+
161
+ Array(value).each_with_index do |val, index|
162
+ next if val.nil?
163
+
164
+ if SPACING_KEYS.include?(key)
165
+ if MARGIN_DIRECTION_KEYS.include?(key)
166
+ raise ArgumentError, "value of #{key} must be between -6 and 6" if (val < -6 || val > 6)
167
+ elsif !((key == :mx || key == :my) && val == :auto)
168
+ raise ArgumentError, "value of #{key} must be between 0 and 6" if (val < 0 || val > 6)
169
+ end
170
+ end
171
+
172
+ dasherized_val = val.to_s.dasherize
173
+ breakpoint = BREAKPOINTS[index]
174
+
175
+ if BOOLEAN_MAPPINGS.has_key?(key)
176
+ BOOLEAN_MAPPINGS[key][:mappings].map { |m| m[:css_class] if m[:value] == val }.compact.each do |css_class|
177
+ memo[:classes] << css_class
178
+ end
179
+ elsif key == BG_KEY
180
+ if val.to_s.starts_with?("#")
181
+ memo[:styles] << "background-color: #{val};"
182
+ else
183
+ memo[:classes] << "bg-#{dasherized_val}"
184
+ end
185
+ elsif key == COLOR_KEY
186
+ if val.to_s.chars.last !~ /\D/
187
+ memo[:classes] << "color-#{dasherized_val}"
188
+ else
189
+ memo[:classes] << "text-#{dasherized_val}"
190
+ end
191
+ elsif key == DISPLAY_KEY
192
+ memo[:classes] << "d#{breakpoint}-#{dasherized_val}"
193
+ elsif key == VERTICAL_ALIGN_KEY
194
+ memo[:classes] << "v-align-#{dasherized_val}"
195
+ elsif key == WORD_BREAK_KEY
196
+ memo[:classes] << "wb-#{dasherized_val}"
197
+ elsif BORDER_KEYS.include?(key)
198
+ memo[:classes] << "border-#{dasherized_val}"
199
+ elsif key == DIRECTION_KEY
200
+ memo[:classes] << "flex#{breakpoint}-#{dasherized_val}"
201
+ elsif key == JUSTIFY_CONTENT_KEY
202
+ formatted_value = val.to_s.gsub(/(flex\_|space\_)/, "")
203
+ memo[:classes] << "flex#{breakpoint}-justify-#{formatted_value}"
204
+ elsif key == ALIGN_ITEMS_KEY
205
+ memo[:classes] << "flex#{breakpoint}-items-#{val.to_s.gsub("flex_", "")}"
206
+ elsif key == FLEX_KEY
207
+ memo[:classes] << "flex-#{val}"
208
+ elsif key == FLEX_GROW_KEY
209
+ memo[:classes] << "flex-grow-#{val}"
210
+ elsif key == FLEX_SHRINK_KEY
211
+ memo[:classes] << "flex-shrink-#{val}"
212
+ elsif key == ALIGN_SELF_KEY
213
+ memo[:classes] << "flex-self-#{val}"
214
+ elsif key == WIDTH_KEY || key == HEIGHT_KEY
215
+ if val == :fit || val == :fill
216
+ memo[:classes] << "#{key}-#{val}"
217
+ else
218
+ memo[key] = val
219
+ end
220
+ elsif TEXT_KEYS.include?(key)
221
+ memo[:classes] << "text-#{dasherized_val}"
222
+ elsif TYPOGRAPHY_KEYS.include?(key)
223
+ memo[:classes] << "f#{dasherized_val}"
224
+ elsif MARGIN_DIRECTION_KEYS.include?(key) && val < 0
225
+ memo[:classes] << "#{key.to_s.dasherize}#{breakpoint}-n#{val.abs}"
226
+ elsif key == BOX_SHADOW_KEY
227
+ if val == true
228
+ memo[:classes] << "box-shadow"
229
+ else
230
+ memo[:classes] << "box-shadow-#{dasherized_val}"
231
+ end
232
+ else
233
+ memo[:classes] << "#{key.to_s.dasherize}#{breakpoint}-#{dasherized_val}"
234
+ end
235
+ end
236
+ end
237
+
238
+ {
239
+ classes: out[:classes].join(" "),
240
+ styles: out[:styles].join(" ")
241
+ }.merge(out.except(:classes, :styles))
242
+ end
243
+ end
244
+ end
245
+ end