pulse_view_components 0.3.0 → 0.4.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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +98 -1
  3. data/README.md +26 -0
  4. data/app/assets/builds/js/pulse_view_components.js +3 -3
  5. data/app/assets/builds/js/pulse_view_components.js.map +1 -1
  6. data/app/assets/builds/pulse_view_components.css +2 -1
  7. data/app/assets/stylesheets/pulse_view_components/application.tailwind.css +44 -4
  8. data/app/assets/stylesheets/pulse_view_components/tokens.primitives.css +68 -0
  9. data/app/assets/stylesheets/pulse_view_components/tokens.system.css +95 -0
  10. data/app/components/pulse/action_list/divider.rb +13 -4
  11. data/app/components/pulse/action_list/form_wrapper.rb +2 -0
  12. data/app/components/pulse/action_list/heading.html.slim +7 -0
  13. data/app/components/pulse/action_list/heading.rb +2 -0
  14. data/app/components/pulse/action_list/item.html.slim +6 -6
  15. data/app/components/pulse/action_list/item.rb +11 -9
  16. data/app/components/pulse/action_list.css +109 -82
  17. data/app/components/pulse/action_list.html.slim +11 -0
  18. data/app/components/pulse/action_list.rb +5 -0
  19. data/app/components/pulse/badge.rb +20 -16
  20. data/app/components/pulse/base_component.rb +25 -1
  21. data/app/components/pulse/blank_slate.rb +4 -2
  22. data/app/components/pulse/border_box/header.rb +5 -3
  23. data/app/components/pulse/border_box.rb +21 -17
  24. data/app/components/pulse/box.rb +2 -0
  25. data/app/components/pulse/breadcrumb_item.rb +3 -1
  26. data/app/components/pulse/breadcrumbs.html.slim +2 -2
  27. data/app/components/pulse/breadcrumbs.rb +2 -0
  28. data/app/components/pulse/button.rb +28 -25
  29. data/app/components/pulse/component.rb +2 -1
  30. data/app/components/pulse/conditional_wrapper.rb +2 -0
  31. data/app/components/pulse/content.rb +2 -0
  32. data/app/components/pulse/counter.css +3 -3
  33. data/app/components/pulse/counter.rb +4 -0
  34. data/app/components/pulse/flash.rb +10 -6
  35. data/app/components/pulse/forms/error_explanation.html.slim +3 -3
  36. data/app/components/pulse/forms/error_explanation.rb +30 -13
  37. data/app/components/pulse/forms/error_explanation.yml +4 -0
  38. data/app/components/pulse/icon.rb +2 -0
  39. data/app/components/pulse/label.rb +5 -3
  40. data/app/components/pulse/layout.css +53 -52
  41. data/app/components/pulse/layout.rb +14 -6
  42. data/app/components/pulse/link.rb +10 -8
  43. data/app/components/pulse/modal.html.slim +20 -20
  44. data/app/components/pulse/modal.rb +10 -8
  45. data/app/components/pulse/modal_controller.js +3 -3
  46. data/app/components/pulse/nav_link.html.slim +7 -7
  47. data/app/components/pulse/nav_link.rb +4 -2
  48. data/app/components/pulse/nav_list/heading.rb +3 -1
  49. data/app/components/pulse/nav_list/item.html.slim +8 -0
  50. data/app/components/pulse/nav_list/item.rb +2 -2
  51. data/app/components/pulse/nav_list.rb +5 -0
  52. data/app/components/pulse/popover.html.slim +1 -1
  53. data/app/components/pulse/popover.rb +65 -26
  54. data/app/components/pulse/progress_bar.html.slim +2 -2
  55. data/app/components/pulse/progress_bar.rb +5 -3
  56. data/app/components/pulse/table/cell.html.slim +2 -2
  57. data/app/components/pulse/table/cell.rb +14 -10
  58. data/app/components/pulse/table/column.rb +2 -0
  59. data/app/components/pulse/table.html.slim +3 -3
  60. data/app/components/pulse/table.rb +19 -16
  61. data/app/components/pulse/text.rb +26 -24
  62. data/app/components/pulse/toast.html.slim +8 -8
  63. data/app/components/pulse/toast.rb +25 -23
  64. data/app/lib/pulse/status.rb +48 -0
  65. data/lib/pulse/view_components/version.rb +1 -1
  66. data/previews/foundations/colors_preview/palettes.html.slim +12 -0
  67. data/previews/foundations/colors_preview/semantic.html.slim +16 -0
  68. data/previews/foundations/colors_preview.rb +55 -0
  69. data/previews/pulse/action_list_preview.rb +77 -0
  70. data/previews/pulse/border_box_preview.rb +1 -1
  71. data/previews/pulse/counter_preview.rb +1 -1
  72. data/previews/pulse/flash_preview.rb +1 -1
  73. data/previews/pulse/forms/error_explanation_preview.rb +49 -0
  74. data/previews/pulse/layout_preview.rb +5 -5
  75. data/previews/pulse/nav_list_preview.rb +1 -1
  76. data/previews/pulse/popover_preview.rb +48 -22
  77. data/previews/pulse/progress_bar_preview.rb +1 -1
  78. data/previews/pulse/table_preview.rb +6 -6
  79. metadata +17 -11
@@ -9,22 +9,52 @@ module Pulse
9
9
  # positioned properly. To render the popover with relative positioning, use
10
10
  # the relative property.
11
11
  class Popover < Pulse::Component
12
+ status :stable
13
+
14
+ # The caret is a white square rotated 45 degrees and drawn over the edge of
15
+ # the body, with a border on the two sides that face outward.
16
+ CARET_BASE = 'pulse:before:absolute pulse:before:size-3 ' \
17
+ 'pulse:before:rotate-45 pulse:before:bg-white ' \
18
+ 'pulse:before:border-neutral-300'
19
+ CARET_TOP = 'pulse:before:-top-1.5 pulse:before:border-t ' \
20
+ 'pulse:before:border-l'
21
+ CARET_BOTTOM = 'pulse:before:-bottom-1.5 pulse:before:border-b ' \
22
+ 'pulse:before:border-r'
23
+ CARET_LEFT = 'pulse:before:-left-1.5 pulse:before:border-b ' \
24
+ 'pulse:before:border-l'
25
+ CARET_RIGHT = 'pulse:before:-right-1.5 pulse:before:border-t ' \
26
+ 'pulse:before:border-r'
27
+ CARET_CENTER_X = 'pulse:before:left-1/2 pulse:before:-translate-x-1/2'
28
+ CARET_CENTER_Y = 'pulse:before:top-1/2 pulse:before:-translate-y-1/2'
29
+
12
30
  CARET_DEFAULT = :top
13
31
  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'
32
+ CARET_DEFAULT => "#{CARET_TOP} #{CARET_CENTER_X}",
33
+ :top_left => "#{CARET_TOP} pulse:before:left-6",
34
+ :top_right => "#{CARET_TOP} pulse:before:right-6",
35
+ :bottom => "#{CARET_BOTTOM} #{CARET_CENTER_X}",
36
+ :bottom_left => "#{CARET_BOTTOM} pulse:before:left-6",
37
+ :bottom_right => "#{CARET_BOTTOM} pulse:before:right-6",
38
+ :left => "#{CARET_LEFT} #{CARET_CENTER_Y}",
39
+ :left_top => "#{CARET_LEFT} pulse:before:top-6",
40
+ :left_bottom => "#{CARET_LEFT} pulse:before:bottom-6",
41
+ :right => "#{CARET_RIGHT} #{CARET_CENTER_Y}",
42
+ :right_top => "#{CARET_RIGHT} pulse:before:top-6",
43
+ :right_bottom => "#{CARET_RIGHT} pulse:before:bottom-6"
44
+ }.freeze
45
+ CARET_OPTIONS = CARET_MAPPINGS.keys.freeze
46
+
47
+ # Leaves room for the caret on the side it points from.
48
+ CARET_MARGIN_MAPPINGS = {
49
+ top: 'pulse:mt-2',
50
+ bottom: 'pulse:mb-2',
51
+ left: 'pulse:ml-2',
52
+ right: 'pulse:mr-2'
26
53
  }.freeze
27
54
 
55
+ DEFAULT_WIDTH = 'pulse:w-64'
56
+ LARGE_WIDTH = 'pulse:w-80'
57
+
28
58
  DEFAULT_HEADING_TAG = :h4
29
59
 
30
60
  # The heading
@@ -36,7 +66,7 @@ module Pulse
36
66
  system_arguments[:type] = :body3
37
67
  system_arguments[:tag] = tag
38
68
  system_arguments[:classes] = merge_classes(
39
- 'pulse-mb-2',
69
+ 'pulse:mb-2',
40
70
  system_arguments[:classes]
41
71
  )
42
72
 
@@ -50,18 +80,19 @@ module Pulse
50
80
  # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
51
81
  renders_one :body, lambda { |caret: CARET_DEFAULT, large: false,
52
82
  **system_arguments|
83
+ caret = fetch_or_fallback(CARET_OPTIONS, caret, CARET_DEFAULT)
84
+ side = caret.to_s.split('_').first.to_sym
85
+ @large = large
86
+
53
87
  @body_arguments = system_arguments
54
88
  @body_arguments['data-caret'] = caret
55
89
  @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'
90
+ 'pulse:relative pulse:bg-white pulse:border pulse:border-neutral-300',
91
+ 'pulse:rounded-lg pulse:p-4 pulse:text-left pulse:shadow-lg',
92
+ CARET_MARGIN_MAPPINGS[side],
93
+ CARET_BASE,
94
+ CARET_MAPPINGS[caret],
95
+ system_arguments[:classes]
65
96
  )
66
97
 
67
98
  Pulse::Content.new
@@ -71,16 +102,24 @@ module Pulse
71
102
  def initialize(**system_arguments)
72
103
  @system_arguments = deny_tag_argument(**system_arguments)
73
104
  @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
105
  end
79
106
 
80
107
  def render?
81
108
  body.present?
82
109
  end
83
110
 
111
+ # Built from the template because the body slot, which sets `large`, is
112
+ # only evaluated once rendering starts.
113
+ def wrapper_arguments
114
+ @system_arguments.merge(
115
+ classes: merge_classes(
116
+ 'pulse:relative pulse:z-10',
117
+ @large ? LARGE_WIDTH : DEFAULT_WIDTH,
118
+ @system_arguments[:classes]
119
+ )
120
+ )
121
+ end
122
+
84
123
  def body_component
85
124
  Pulse::Box.new(**@body_arguments)
86
125
  end
@@ -1,5 +1,5 @@
1
1
  /! components/pulse/progress_bar
2
2
  = render Pulse::BaseComponent.new(**@system_attributes) do
3
- div class="pulse-h-1 pulse-bg-success" style="width: #{[@percentage, 100].min}%"
3
+ div class="pulse:h-1 pulse:bg-success" style="width: #{[@percentage, 100].min}%"
4
4
  - if @percentage > 100
5
- div class="pulse-h-1 pulse-bg-error" style="width: #{@percentage - 100}%"
5
+ div class="pulse:h-1 pulse:bg-error" style="width: #{@percentage - 100}%"
@@ -3,13 +3,15 @@
3
3
  module Pulse
4
4
  # Renders a simple progress bar
5
5
  class ProgressBar < Pulse::Component
6
+ status :stable
7
+
6
8
  def initialize(percentage: 0, **system_attributes)
7
9
  @percentage = percentage.to_i
8
10
  @system_attributes = system_attributes
9
11
  @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',
12
+ @system_attributes[:classes] = merge_classes(
13
+ 'pulse:flex pulse:flex-row pulse:h-1 pulse:w-full pulse:bg-neutral-300',
14
+ 'pulse:dark:bg-neutral-600',
13
15
  system_attributes[:classes]
14
16
  )
15
17
  end
@@ -1,9 +1,9 @@
1
1
  /! components/pulse/tables/cell
2
2
  = render Pulse::BaseComponent.new(**options) do
3
3
  - if sortable?
4
- = link_to request.params.merge(**url_options), class: merge_classes('pulse-inline-flex pulse-items-center pulse-gap-1', title_classes) do
4
+ = link_to request.params.merge(**url_options), class: merge_classes('pulse:inline-flex pulse:items-center pulse:gap-1', title_classes) do
5
5
  = content
6
- = render Pulse::Icon.new(icon: icon, width: 9, height: 8, classes: 'pulse-shrink-0', title: t(".#{icon}"))
6
+ = render Pulse::Icon.new(icon: icon, width: 9, height: 8, classes: 'pulse:shrink-0', title: t(".#{icon}"))
7
7
 
8
8
  - else
9
9
  span class=title_classes
@@ -4,24 +4,28 @@ module Pulse
4
4
  class Table
5
5
  # Renders a table cell
6
6
  class Cell < Pulse::Component
7
+ status :stable
8
+
7
9
  attr_reader :sort_by, :current_sort_column, :current_sort_direction,
8
10
  :additional_params, :footer
9
11
 
10
12
  HEAD_DEFAULT_SCHEME = :xs
11
13
  HEAD_SCHEME_MAPPINGS = {
12
- sm: 'pulse-px-3 pulse-py-4 print:pulse-px-2 print:pulse-py-3 ' \
13
- 'pulse-text-sm print:pulse-text-xs pulse-font-base',
14
- xs: 'pulse-px-2 pulse-py-4 print:pulse-py-2 pulse-text-xs ' \
15
- 'print:pulse-text-2xs pulse-font-medium'
14
+ sm: 'pulse:px-3 pulse:py-4 pulse:print:px-2 pulse:print:py-3 ' \
15
+ 'pulse:text-sm pulse:print:text-xs pulse:font-normal',
16
+ xs: 'pulse:px-2 pulse:py-4 pulse:print:py-2 pulse:text-xs ' \
17
+ 'pulse:print:text-2xs pulse:font-medium'
16
18
  }.freeze
17
19
  HEAD_SCHEME_OPTIONS = HEAD_SCHEME_MAPPINGS.keys.freeze
18
20
 
19
- DEFAULT_CELL_CLASSES = 'pulse-text-start first:pulse-ps-4 ' \
20
- 'last:pulse-pe-4 pulse-px-2 pulse-text-gray ' \
21
- 'print:first:pulse-ps-2 print:last:pulse-pe-2'
22
- DEFAULT_TH_CLASSES = 'pulse-uppercase pulse-tracking-wider'
23
- DEFAULT_TD_CLASSES = 'pulse-py-2 pulse-whitespace-nowrap ' \
24
- 'pulse-text-sm print:pulse-text-xs pulse-text-gray'
21
+ DEFAULT_CELL_CLASSES = 'pulse:text-start pulse:first:ps-4 ' \
22
+ 'pulse:last:pe-4 pulse:px-2 ' \
23
+ 'pulse:text-neutral-700 ' \
24
+ 'pulse:print:first:ps-2 pulse:print:last:pe-2'
25
+ DEFAULT_TH_CLASSES = 'pulse:uppercase pulse:tracking-wider'
26
+ DEFAULT_TD_CLASSES = 'pulse:py-2 pulse:whitespace-nowrap ' \
27
+ 'pulse:text-sm pulse:print:text-xs ' \
28
+ 'pulse:text-neutral-700'
25
29
 
26
30
  def initialize(sort_by: nil, current_sort_column: nil,
27
31
  current_sort_direction: nil, additional_params: {},
@@ -4,6 +4,8 @@ module Pulse
4
4
  class Table
5
5
  # Encapsulates table column properties
6
6
  class Column < Pulse::Component
7
+ status :stable
8
+
7
9
  attr_reader :title, :sr_title, :sort_by, :footer, :options
8
10
 
9
11
  def initialize(title: '', sr_title: nil, sort_by: nil, footer: nil,
@@ -1,7 +1,7 @@
1
1
  /! components/pulse/table
2
2
  - if collection.present?
3
- div class="pulse-flex pulse-flex-col pulse-gap-4"
4
- div class=class_names("pulse-w-full pulse-shadow print:pulse-shadow-none sm:pulse-rounded-lg", 'pulse-overflow-x-scroll': scroll)
3
+ div class="pulse:flex pulse:flex-col pulse:gap-4"
4
+ div class=class_names("pulse:w-full pulse:shadow pulse:print:shadow-none pulse:sm:rounded-lg", 'pulse:overflow-x-scroll': scroll)
5
5
  = render Pulse::BaseComponent.new(**table_options) do
6
6
  - if header
7
7
  = render Pulse::BaseComponent.new(tag: :thead, classes: head_classes) do
@@ -39,7 +39,7 @@
39
39
 
40
40
 
41
41
  - if pagination? && collection.respond_to?(:total_pages)
42
- div class="pulse-text-center pulse-text-gray"
42
+ div class="pulse:text-center pulse:text-neutral-700"
43
43
  = paginate(collection, window: 9)
44
44
 
45
45
  - else
@@ -5,18 +5,21 @@ module Pulse
5
5
  # Inspiration taken from
6
6
  # https://github.com/baoagency/polaris_view_components/
7
7
  class Table < Pulse::Component
8
- DEFAULT_TABLE_CLASSES = 'pulse-min-w-full pulse-divide-y ' \
9
- 'pulse-divide-gray-300'
10
- DEFAULT_HEAD_CLASSES = 'pulse-bg-gray-100'
11
- DEFAULT_TH_CLASSES = 'pulse-text-start pulse-text-gray pulse-uppercase ' \
12
- 'pulse-tracking-wider pulse-px-4 print:pulse-px-2 ' \
13
- 'pulse-py-4 print:pulse-py-3 text-xs pulse-font-medium'
14
- DEFAULT_BODY_CLASSES = 'pulse-bg-white pulse-divide-y pulse-text-sm ' \
15
- 'pulse-leading-extra-snug pulse-divide-gray-300 ' \
16
- 'print:pulse-divide-gray-400 pulse-tracking-tight ' \
17
- ''
18
- DEFAULT_ROW_CLASSES = 'pulse-border-secondary-300'
19
- DEFAULT_TD_CLASSES = 'pulse-px-4 print:pulse-px-2 pulse-py-4'
8
+ status :stable
9
+
10
+ DEFAULT_TABLE_CLASSES = 'pulse:min-w-full pulse:divide-y ' \
11
+ 'pulse:divide-neutral-300'
12
+ DEFAULT_HEAD_CLASSES = 'pulse:bg-neutral-100'
13
+ DEFAULT_TH_CLASSES = 'pulse:text-start pulse:text-neutral-700 ' \
14
+ 'pulse:uppercase ' \
15
+ 'pulse:tracking-wider pulse:px-4 pulse:print:px-2 ' \
16
+ 'pulse:py-4 pulse:print:py-3 pulse:text-xs ' \
17
+ 'pulse:font-medium'
18
+ DEFAULT_BODY_CLASSES = 'pulse:bg-white pulse:divide-y pulse:text-sm ' \
19
+ 'pulse:leading-snug pulse:divide-neutral-300 ' \
20
+ 'pulse:print:divide-neutral-400 pulse:tracking-tight'
21
+ DEFAULT_ROW_CLASSES = 'pulse:border-secondary-300'
22
+ DEFAULT_TD_CLASSES = 'pulse:px-4 pulse:print:px-2 pulse:py-4'
20
23
 
21
24
  delegate :paginate, to: :helpers
22
25
 
@@ -58,7 +61,7 @@ module Pulse
58
61
  { tag: 'table' }.deep_merge(@options).tap do |args|
59
62
  args[:classes] = merge_classes(
60
63
  DEFAULT_TABLE_CLASSES,
61
- 'pulse-container pulse-mx-auto',
64
+ 'pulse:container pulse:mx-auto',
62
65
  args[:classes]
63
66
  )
64
67
  args[:data] ||= {}
@@ -81,8 +84,8 @@ module Pulse
81
84
  def body_classes
82
85
  merge_classes(
83
86
  DEFAULT_BODY_CLASSES,
84
- '[&>tr:nth-child(even)]:pulse-bg-gray-50': striped,
85
- 'pulse-border-b [&>tr]:pulse-border-t [&>tr:first-child]:pulse-border-t-0': !striped # rubocop:disable Layout/LineLength
87
+ 'pulse:[&>tr:nth-child(even)]:bg-neutral-50': striped,
88
+ 'pulse:border-b pulse:[&>tr]:border-t pulse:[&>tr:first-child]:border-t-0': !striped # rubocop:disable Layout/LineLength
86
89
  )
87
90
  end
88
91
 
@@ -90,7 +93,7 @@ module Pulse
90
93
  { tag: 'tr' }.tap do |args|
91
94
  args[:classes] = merge_classes(
92
95
  DEFAULT_ROW_CLASSES,
93
- { 'hover:pulse-bg-dark-charcoal-50': hoverable },
96
+ { 'pulse:hover:bg-neutral-50': hoverable },
94
97
  row_classes
95
98
  )
96
99
  args[:id] = dom_id(row) if row.respond_to?(:to_key)
@@ -3,31 +3,33 @@
3
3
  module Pulse
4
4
  # Base Text component
5
5
  class Text < Pulse::Component
6
+ status :stable
7
+
6
8
  STYLE_MAPPINGS = {
7
- h1: 'pulse-text-3xl pulse-font-bold print:pulse-text-2xl',
8
- h2: 'pulse-text-2xl pulse-font-bold print:pulse-text-xl',
9
- h3: 'pulse-text-xl pulse-font-bold print:pulse-text-lg',
10
- h4: 'pulse-text-lg pulse-font-bold print:pulse-text-base',
11
- subtitle: 'pulse-text-xl print:pulse-text-lg pulse-leading-tight ' \
12
- 'pulse-font-bold tracking-tight',
13
- body1: 'pulse-text-lg print:pulse-text-base pulse-font-bold ' \
14
- 'pulse-leading-6 tracking-tight',
15
- body2: 'pulse-text-lg print:pulse-text-base pulse-font-normal ' \
16
- 'pulse-leading-6 tracking-tight',
17
- body3: 'pulse-text-base print:pulse-text-sm pulse-font-bold ' \
18
- 'pulse-leading-normal',
19
- body4: 'pulse-text-base print:pulse-text-sm pulse-font-normal ' \
20
- 'pulse-leading-normal',
21
- body5: 'pulse-text-sm print:pulse-text-xs pulse-font-bold ' \
22
- 'pulse-leading-snug',
23
- body6: 'pulse-text-sm print:pulse-text-xs pulse-font-normal ' \
24
- 'pulse-leading-snug',
25
- body7: 'pulse-text-xs pulse-font-bold pulse-leading-5',
26
- body8: 'pulse-text-xs pulse-font-normal pulse-leading-5',
27
- caption1: 'pulse-text-2xs pulse-font-bold tracking-normal ' \
28
- 'pulse-leading-extra-tight',
29
- caption2: 'pulse-text-2xs pulse-font-normal tracking-normal ' \
30
- 'pulse-leading-extra-tight'
9
+ h1: 'pulse:text-3xl pulse:font-bold pulse:print:text-2xl',
10
+ h2: 'pulse:text-2xl pulse:font-bold pulse:print:text-xl',
11
+ h3: 'pulse:text-xl pulse:font-bold pulse:print:text-lg',
12
+ h4: 'pulse:text-lg pulse:font-bold pulse:print:text-base',
13
+ subtitle: 'pulse:text-xl pulse:print:text-lg pulse:leading-tight ' \
14
+ 'pulse:font-bold pulse:tracking-tight',
15
+ body1: 'pulse:text-lg pulse:print:text-base pulse:font-bold ' \
16
+ 'pulse:leading-6 pulse:tracking-tight',
17
+ body2: 'pulse:text-lg pulse:print:text-base pulse:font-normal ' \
18
+ 'pulse:leading-6 pulse:tracking-tight',
19
+ body3: 'pulse:text-base pulse:print:text-sm pulse:font-bold ' \
20
+ 'pulse:leading-normal',
21
+ body4: 'pulse:text-base pulse:print:text-sm pulse:font-normal ' \
22
+ 'pulse:leading-normal',
23
+ body5: 'pulse:text-sm pulse:print:text-xs pulse:font-bold ' \
24
+ 'pulse:leading-snug',
25
+ body6: 'pulse:text-sm pulse:print:text-xs pulse:font-normal ' \
26
+ 'pulse:leading-snug',
27
+ body7: 'pulse:text-xs pulse:font-bold pulse:leading-5',
28
+ body8: 'pulse:text-xs pulse:font-normal pulse:leading-5',
29
+ caption1: 'pulse:text-2xs pulse:font-bold pulse:tracking-normal ' \
30
+ 'pulse:leading-tight',
31
+ caption2: 'pulse:text-2xs pulse:font-normal pulse:tracking-normal ' \
32
+ 'pulse:leading-tight'
31
33
  }.freeze
32
34
 
33
35
  TAG_MAPPINGS = {
@@ -1,14 +1,14 @@
1
1
  /! components/pulse/toast
2
2
  = render Pulse::BaseComponent.new(**@system_arguments) do
3
- div class="pulse-flex pulse-w-full" data-theme=(@theme)
4
- div class="pulse-flex-1 pulse-flex pulse-justify-between pulse-items-start"
5
- div class="pulse-flex-1 pulse-flex pulse-justify-between pulse-items-center pulse-gap-4"
6
- = render Pulse::Text.new(type: :body6, classes: 'pulse-mb-0 pulse-min-w-[250px]') do
3
+ div class="pulse:flex pulse:w-full" data-theme=(@theme)
4
+ div class="pulse:flex-1 pulse:flex pulse:justify-between pulse:items-start"
5
+ div class="pulse:flex-1 pulse:flex pulse:justify-between pulse:items-center pulse:gap-4"
6
+ = render Pulse::Text.new(type: :body6, classes: 'pulse:mb-0 pulse:min-w-62.5') do
7
7
  = content
8
- div class="pulse-mt-0"
9
- div class="pulse-flex pulse-items-center pulse-gap-3.5 -pulse-my-2"
8
+ div class="pulse:mt-0"
9
+ div class="pulse:flex pulse:items-center pulse:gap-3.5 pulse:-my-2"
10
10
  - if action?
11
- = render Pulse::Text.new(type: :body5, classes: merge_classes(link_classes, 'pulse-uppercase pulse-mb-0')) do
11
+ = render Pulse::Text.new(type: :body5, classes: merge_classes(link_classes, 'pulse:uppercase pulse:mb-0')) do
12
12
  = action
13
- button class="pulse-appearance-none pulse-bg-transparent pulse-border-0 pulse-p-2" data-action="click->pulse--toast#dismiss"
13
+ button class="pulse:appearance-none pulse:bg-transparent pulse:border-0 pulse:p-2" data-action="click->pulse--toast#dismiss"
14
14
  = render Pulse::Icon.new(icon: :close, width: 9, height: 9, class: link_classes)
@@ -3,6 +3,8 @@
3
3
  module Pulse
4
4
  # Renders a toast popup.
5
5
  class Toast < Pulse::Component
6
+ status :stable
7
+
6
8
  include Turbo::FramesHelper
7
9
 
8
10
  renders_one :action, lambda { |url:, **system_arguments, &block|
@@ -16,15 +18,15 @@ module Pulse
16
18
  }
17
19
 
18
20
  LINK_MAPPINGS = {
19
- default: 'pulse-text-white hover:pulse-text-white',
20
- error: 'pulse-text-gray hover:pulse-text-gray',
21
- success: 'pulse-text-white hover:pulse-text-white'
21
+ default: 'pulse:text-white pulse:hover:text-white',
22
+ error: 'pulse:text-neutral-700 pulse:hover:text-neutral-700',
23
+ success: 'pulse:text-white pulse:hover:text-white'
22
24
  }.freeze
23
25
 
24
26
  THEME_MAPPINGS = {
25
- default: 'pulse-bg-gray-700 pulse-text-white',
26
- error: 'pulse-bg-error-100 pulse-text-gray-900',
27
- success: 'pulse-bg-gray-700 pulse-text-white'
27
+ default: 'pulse:bg-neutral-700 pulse:text-white',
28
+ error: 'pulse:bg-error-100 pulse:text-neutral-900',
29
+ success: 'pulse:bg-neutral-700 pulse:text-white'
28
30
  }.freeze
29
31
  DEFAULT_THEME = :default
30
32
 
@@ -47,14 +49,14 @@ module Pulse
47
49
  def class_attributes
48
50
  merge_classes(
49
51
  @system_arguments[:classes],
50
- # 'pulse-hidden',
51
- 'pulse-pl-8',
52
- 'pulse-pr-4',
53
- 'pulse-py-2.5',
54
- 'pulse-mb-4',
55
- 'pulse-rounded',
56
- 'pulse-shadow-toast',
57
- 'pulse-z-100',
52
+ # 'pulse:hidden',
53
+ 'pulse:pl-8',
54
+ 'pulse:pr-4',
55
+ 'pulse:py-2.5',
56
+ 'pulse:mb-4',
57
+ 'pulse:rounded',
58
+ 'pulse:shadow-md',
59
+ 'pulse:z-100',
58
60
  THEME_MAPPINGS[@theme]
59
61
  )
60
62
  end
@@ -65,15 +67,15 @@ module Pulse
65
67
  {
66
68
  controller: 'pulse--toast',
67
69
  pulse__toast_auto_hide_value: @auto_hide,
68
- transition_enter: 'pulse-transition pulse-ease-out ' \
69
- 'pulse-duration-500',
70
- transition_enter_start: 'pulse-transform pulse-opacity-0 ' \
71
- 'pulse-translate-y-full',
72
- transition_enter_end: 'pulse-transform pulse-opacity-100 ' \
73
- 'pulse-translate-y-0',
74
- transition_leave: 'pulse-transition pulse-ease-in pulse-duration-200',
75
- transition_leave_start: 'pulse-transform pulse-opacity-100',
76
- transition_leave_end: 'pulse-transform pulse-opacity-0'
70
+ transition_enter: 'pulse:transition pulse:ease-out ' \
71
+ 'pulse:duration-500',
72
+ transition_enter_start: 'pulse:transform pulse:opacity-0 ' \
73
+ 'pulse:translate-y-full',
74
+ transition_enter_end: 'pulse:transform pulse:opacity-100 ' \
75
+ 'pulse:translate-y-0',
76
+ transition_leave: 'pulse:transition pulse:ease-in pulse:duration-200',
77
+ transition_leave_start: 'pulse:transform pulse:opacity-100',
78
+ transition_leave_end: 'pulse:transform pulse:opacity-0'
77
79
  }
78
80
  )
79
81
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pulse
4
+ # Declares how complete a component is, following Primer's component
5
+ # lifecycle. Components are `:alpha` until they declare otherwise, so a new
6
+ # component is never presented as safe by accident.
7
+ #
8
+ # - `:alpha` Incomplete. Options or features may be broken, unstyled, or
9
+ # missing, and the API may change without notice.
10
+ # - `:beta` Core usage works. Some documented options are not implemented.
11
+ # - `:stable` Complete and safe to use.
12
+ # - `:deprecated` Scheduled for removal.
13
+ #
14
+ # ```ruby
15
+ # class Pulse::Thing < Pulse::Component
16
+ # status :beta
17
+ # end
18
+ #
19
+ # Pulse::Thing.status # => :beta
20
+ # ```
21
+ module Status
22
+ extend ActiveSupport::Concern
23
+
24
+ STATUSES = [:alpha, :beta, :stable, :deprecated].freeze
25
+ DEFAULT_STATUS = :alpha
26
+
27
+ included do
28
+ # Set once while each class body loads, never at request time.
29
+ # rubocop:disable-next ThreadSafety/ClassAndModuleAttributes
30
+ class_attribute :component_status, instance_writer: false,
31
+ default: DEFAULT_STATUS
32
+ end
33
+
34
+ class_methods do
35
+ def status(value = nil)
36
+ return component_status if value.nil?
37
+
38
+ unless STATUSES.include?(value)
39
+ raise ArgumentError,
40
+ "Unknown status #{value.inspect}. Use one of " \
41
+ "#{STATUSES.to_sentence}."
42
+ end
43
+
44
+ self.component_status = value
45
+ end
46
+ end
47
+ end
48
+ end
@@ -5,7 +5,7 @@ module Pulse
5
5
  module ViewComponents
6
6
  module VERSION
7
7
  MAJOR = 0
8
- MINOR = 3
8
+ MINOR = 4
9
9
  PATCH = 0
10
10
 
11
11
  STRING = [MAJOR, MINOR, PATCH].join('.')
@@ -0,0 +1,12 @@
1
+ div class="pulse:flex pulse:flex-col pulse:gap-6"
2
+ p class="pulse:text-sm pulse:text-neutral-600"
3
+ ' Primitive palettes have no utility classes. Reach them through a semantic name.
4
+ - palettes.each do |name, swatches|
5
+ section
6
+ h2 class="pulse:mb-2 pulse:text-lg pulse:font-bold pulse:text-neutral-900"
7
+ code class="pulse:font-mono" = name
8
+ div class="pulse:grid pulse:grid-cols-10 pulse:gap-1"
9
+ - swatches.each do |swatch|
10
+ div class="pulse:flex pulse:flex-col pulse:justify-end pulse:h-20 pulse:p-2 pulse:rounded pulse:text-xs pulse:font-mono" style="background-color: #{swatch.hex}; color: #{swatch.ink}" title="#{name}-#{swatch.shade}"
11
+ span = swatch.shade
12
+ span = swatch.hex
@@ -0,0 +1,16 @@
1
+ div class="pulse:flex pulse:flex-col pulse:gap-6"
2
+ - semantic.each do |name, (palette, default_shade)|
3
+ section
4
+ h2 class="pulse:text-lg pulse:font-bold pulse:text-neutral-900"
5
+ = name
6
+ p class="pulse:mb-2 pulse:text-sm pulse:text-neutral-600"
7
+ ' Maps to
8
+ code class="pulse:font-mono" = palette
9
+ ' . Bare
10
+ code class="pulse:font-mono" = "pulse:bg-#{name}"
11
+ | is shade #{default_shade}.
12
+ div class="pulse:grid pulse:grid-cols-10 pulse:gap-1"
13
+ - palettes.fetch(palette).each do |swatch|
14
+ div class=class_names('pulse:flex pulse:flex-col pulse:justify-end pulse:h-20 pulse:p-2 pulse:rounded pulse:text-xs pulse:font-mono', 'pulse:ring-2 pulse:ring-offset-2 pulse:ring-neutral-900' => swatch.shade == default_shade) style="background-color: #{swatch.hex}; color: #{swatch.ink}" title="#{name}-#{swatch.shade}"
15
+ span = "#{name}-#{swatch.shade}"
16
+ span = swatch.hex
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foundations
4
+ # Reads the token stylesheets rather than the built CSS, because Tailwind
5
+ # only emits the theme variables that some class uses.
6
+ #
7
+ # @label Colors
8
+ class ColorsPreview < ViewComponent::Preview
9
+ Swatch = Data.define(:shade, :hex) do
10
+ # Dark text on light swatches, using relative luminance.
11
+ def ink
12
+ r, g, b = hex.delete('#').scan(/../).map { it.to_i(16) / 255.0 }
13
+ luminance = (0.2126 * r) + (0.7152 * g) + (0.0722 * b)
14
+ luminance > 0.55 ? '#111827' : '#ffffff'
15
+ end
16
+ end
17
+
18
+ TOKENS_PATH = Pulse::ViewComponents::Engine.root.join(
19
+ 'app/assets/stylesheets/pulse_view_components'
20
+ )
21
+ PALETTE_PATTERN = /--palette-([a-z-]+)-(\d+):\s*(#\h{6});/
22
+ SEMANTIC_PATTERN = /--color-([a-z]+):\s*--theme\(--palette-([a-z-]+)-(\d+)/
23
+
24
+ # @label Semantic
25
+ def semantic
26
+ render_with_template(
27
+ locals: { semantic: semantic_tokens, palettes: palette_tokens }
28
+ )
29
+ end
30
+
31
+ # @label Palettes
32
+ def palettes
33
+ render_with_template(locals: { palettes: palette_tokens })
34
+ end
35
+
36
+ private
37
+
38
+ # { 'coral-red' => [Swatch('50', '#ffecec'), ...], ... }
39
+ def palette_tokens
40
+ TOKENS_PATH.join('tokens.primitives.css').read
41
+ .scan(PALETTE_PATTERN)
42
+ .group_by(&:first)
43
+ .transform_values do |rows|
44
+ rows.map { |_, shade, hex| Swatch.new(shade:, hex:) }
45
+ end
46
+ end
47
+
48
+ # { 'primary' => ['coral-red', '700'], ... }
49
+ def semantic_tokens
50
+ TOKENS_PATH.join('tokens.system.css').read
51
+ .scan(SEMANTIC_PATTERN)
52
+ .to_h { |name, palette, shade| [name, [palette, shade]] }
53
+ end
54
+ end
55
+ end