relay_ui 0.4.1 → 0.4.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac008514b4c6294d178fbcb34b555bbb87f55e71a58ae882f772da69766e9afa
4
- data.tar.gz: 49972be6884ac11eb5a932afb0b2e1fa803dcac29f7fc5690f54db358f74bd20
3
+ metadata.gz: 9a75842c1f5810806bc506c8ca23a86cc8a2a3006e9c6b5855dc2d13ad18b566
4
+ data.tar.gz: 32856878ae638a4e506ce7737d0ed4a3a8869bace13afb9d189aa27653bbdc56
5
5
  SHA512:
6
- metadata.gz: e13a3dc12a61096d944ce0d88da1416a9a4a9e05127b1d4cc37a77dbb414629ceee63a02a5d0f87a74cb8488ced3f2d2eda080f49e5f65b5d1abe0f9f5d7d59e
7
- data.tar.gz: 5d9f7449fd5c1cb535527fa54ca04e531e4f55b961f18eed4e5f2b02bdb0b6603ab92bb0a0a56ebbbac1df6fdb3b59d04ae68be3670a60a4f148febfe4a2f115
6
+ metadata.gz: 5f727bcd802a52b62b2a1d24b3f8847be1cbb54663aa9d70a2d229a65bf643da7175bfbd797b582975c86d124a5718eaecbfdbea3daabc49aeb22ef54823d636
7
+ data.tar.gz: 0ec8ebcb52f7b2a08ad11473b41990cd122dc8260387200584ff8e79fa07cc424cb2bc8fff52f7eabd4970cde06d13ea7237fec69d082a9109c7ba0bff7bad81
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rui/forms/tailwind_form_builder"
3
+ require "rui/forms/form_builder"
4
4
 
5
5
  module RUI
6
6
  if defined?(Rails)
@@ -8,7 +8,7 @@ module RUI
8
8
  isolate_namespace RUI
9
9
 
10
10
  initializer "relay_ui.set_default_form_builder" do |app|
11
- app.config.action_view.default_form_builder = RUI::Forms::TailwindFormBuilder
11
+ app.config.action_view.default_form_builder = RUI::Forms::FormBuilder
12
12
  end
13
13
 
14
14
  initializer "relay_ui.autoload.components" do
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RUI
4
- VERSION = "0.4.1"
4
+ VERSION = "0.4.2"
5
5
  end
@@ -0,0 +1,21 @@
1
+ class RUI::Forms::Errors < RUI::Base
2
+ include Phlex::Rails::Helpers::Pluralize
3
+
4
+ def initialize(errors, object_name = "record")
5
+ @errors = errors
6
+ @object_name = object_name
7
+ end
8
+
9
+ def view_template
10
+ if @errors.any?
11
+ div(class: "bg-red-50 text-red-500 p-10 font-medium rounded-md mt-3") do
12
+ h2 { pluralize(@errors.count, "error") + " prohibited this #{@object_name} from being saved:" }
13
+ ul(class: "list-disc ml-6") do
14
+ @errors.each do |error|
15
+ li { error.full_message }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,5 @@
1
1
  class RUI::Forms::FieldGroup < RUI::Base
2
2
  def view_template(&)
3
- div(class: "flex flex-col gap-1", &)
3
+ div(class: "flex flex-col gap-2", &)
4
4
  end
5
5
  end
@@ -0,0 +1,66 @@
1
+ module RUI::Forms
2
+ class FormBuilder < ActionView::Helpers::FormBuilder
3
+ # include ActionView::Helpers::TagHelper
4
+ include ActiveSupport::Concern
5
+ # Base styles for different element types
6
+ TEXT_FIELD_STYLE = "bg-zinc-50 ring ring-zinc-300 hover:ring-zinc-400 focus:ring-blue-500 rounded px-2 py-1".freeze
7
+ SELECT_FIELD_STYLE = "bg-zinc-50 ring ring-zinc-300 hover:ring-zinc-400 focus:ring-blue-500 rounded px-2 py-2".freeze
8
+
9
+ class_attribute :text_field_helpers, default: field_helpers - [ :label, :check_box, :radio_button, :fields_for, :fields, :hidden_field, :file_field ]
10
+
11
+ # Style labels and include error messages if present
12
+ def label(method, content_or_options = nil, options = {}, &block)
13
+ opts = content_or_options || options
14
+
15
+ default_classes = opts[:required] ? "text-sm font-semibold after:content-['*'] after:ml-1 after:text-red-500" : "text-sm font-semibold"
16
+ error_classes = "text-sm text-red-500 font-semibold"
17
+
18
+ label_options = merge_classes(opts, default_classes)
19
+
20
+ label_content = if object.present? && object.errors[method].present?
21
+ @template.content_tag(:div, class: "flex flex-col") do
22
+ @template.concat(super(method, content_or_options, label_options, &block))
23
+ @template.concat(@template.content_tag(:span, object.errors.full_messages_for(method).to_sentence, class: error_classes))
24
+ end
25
+ else
26
+ super(method, content_or_options, label_options, &block)
27
+ end
28
+
29
+ # Use metaprogramming to style fields that are like text fields
30
+
31
+ text_field_helpers.each do |field_method|
32
+ class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
33
+ def #{field_method}(method, options = {})
34
+ super(method, merge_classes(options, TEXT_FIELD_STYLE))
35
+ end
36
+ RUBY_EVAL
37
+ end
38
+ label_content
39
+ end
40
+
41
+ # Fields and helpers that are not covered in text-like fields
42
+ def select(method, choices = nil, options = {}, html_options = {}, &block)
43
+ super(method, choices, options, merge_classes(html_options, SELECT_FIELD_STYLE), &block)
44
+ end
45
+
46
+ def submit(value = nil, options = {})
47
+ default_classes = RUI::TailwindMerger.instance.merge(RUI::Buttons::Base::STYLE, RUI::Buttons::Primary::STYLE)
48
+
49
+ @template.content_tag("div", super(value, merge_classes(options, default_classes)))
50
+ end
51
+
52
+ private
53
+
54
+ # Utility for merging optional classes
55
+ def merge_classes(hash, default_classes)
56
+ if hash.nil?
57
+ { class: default_classes }
58
+ elsif hash.key?(:class)
59
+ merged_classes = RUI::TailwindMerger.instance.merge(default_classes, hash[:class])
60
+ hash.merge!(class: merged_classes)
61
+ else
62
+ hash.merge!(class: default_classes)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,5 +1,21 @@
1
1
  class RUI::Layout::Main < RUI::Base
2
+ def initialize(attrs = {})
3
+ @attrs = attrs
4
+ end
5
+
2
6
  def view_template(&)
3
- main(class: "p-5 lg:p-10 lg:ml-72 w-full max-w-[800px]", &)
7
+ main(**merged_attributes, &)
8
+ end
9
+
10
+ def merged_attributes
11
+ default_classes = "p-10 w-full lg:ml-72 lg:w-[calc(100%_-_18rem)]"
12
+
13
+ merged_classes = if @attrs[:class]
14
+ RUI::TailwindMerger.instance.merge(default_classes, @attrs[:class])
15
+ else
16
+ default_classes
17
+ end
18
+
19
+ @attrs.merge! class: merged_classes
4
20
  end
5
21
  end
@@ -1,5 +1,5 @@
1
1
  class RUI::Layout::Page < RUI::Base
2
2
  def view_template(&)
3
- div(class: "flex flex-row mt-25.5 lg:mt-14", &)
3
+ div(class: "flex mt-25.5 lg:mt-14", &)
4
4
  end
5
5
  end
@@ -8,14 +8,16 @@ class RUI::Links::Base < RUI::Base
8
8
  end
9
9
 
10
10
  def view_template
11
- a(href: @href, class: classes, **@attrs) do
12
- div(class: "flex flex-row items-center gap-2") do
13
- if @icon
14
- div(class: "size-4 my-1") do
15
- render RUI::Icon.new(@icon)
11
+ div do
12
+ a(href: @href, class: classes, **@attrs) do
13
+ div(class: "flex flex-row items-center gap-2") do
14
+ if @icon
15
+ div(class: "size-4 my-1") do
16
+ render RUI::Icon.new(@icon)
17
+ end
16
18
  end
19
+ span { yield } if block_given?
17
20
  end
18
- span { yield } if block_given?
19
21
  end
20
22
  end
21
23
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RUI::Links::Text < RUI::Base
4
+ def initialize(href: "#", **attrs)
5
+ @href = href
6
+ @attrs = attrs
7
+ end
8
+
9
+ def view_template(&) = a(href: @href, class: "text-blue-700 hover:cursor-pointer hover:underline", **@attrs, &)
10
+
11
+ private
12
+
13
+ def classes
14
+ ""
15
+ end
16
+ end
@@ -2,8 +2,8 @@
2
2
 
3
3
  class RUI::Navigation::Top < RUI::Base
4
4
  def view_template(&)
5
- div(class: "fixed top-0 left-0 right-0 bg-white flex flex-col z-50") do
6
- section(class: "flex flex-row items-center justify-between gap-3 border-b border-zinc-300 p-3 lg:px-10", &)
5
+ div(class: "fixed top-0 left-0 right-0 bg-white flex flex-col z-30") do
6
+ section(class: "flex flex-row items-center justify-between gap-3 border-b border-zinc-300 p-3 lg:px-10 min-h-16", &)
7
7
  section(class: "lg:hidden p-3 border-b border-zinc-300") do
8
8
  render RUI::Buttons::Ghost.new(
9
9
  icon: "menu",
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: relay_ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - logicrelay
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-04-10 00:00:00.000000000 Z
10
+ date: 2025-04-11 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: phlex
@@ -226,18 +226,9 @@ files:
226
226
  - lib/rui/card.rb
227
227
  - lib/rui/flash.rb
228
228
  - lib/rui/forms.rb
229
- - lib/rui/forms/checkbox.rb
230
- - lib/rui/forms/email.rb
229
+ - lib/rui/forms/errors.rb
231
230
  - lib/rui/forms/field_group.rb
232
- - lib/rui/forms/helpers.rb
233
- - lib/rui/forms/label.rb
234
- - lib/rui/forms/password.rb
235
- - lib/rui/forms/phone.rb
236
- - lib/rui/forms/radio.rb
237
- - lib/rui/forms/select.rb
238
- - lib/rui/forms/tailwind_form_builder.rb
239
- - lib/rui/forms/text.rb
240
- - lib/rui/forms/textarea.rb
231
+ - lib/rui/forms/form_builder.rb
241
232
  - lib/rui/helpers.rb
242
233
  - lib/rui/icon.rb
243
234
  - lib/rui/layout.rb
@@ -251,6 +242,7 @@ files:
251
242
  - lib/rui/links/outline.rb
252
243
  - lib/rui/links/primary.rb
253
244
  - lib/rui/links/secondary.rb
245
+ - lib/rui/links/text.rb
254
246
  - lib/rui/markdown.rb
255
247
  - lib/rui/markdown/generator.rb
256
248
  - lib/rui/markdown/safe.rb
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class RUI::Forms::Checkbox < RUI::Base
4
- def initialize(**kwargs)
5
- @kwargs = kwargs
6
- end
7
-
8
- def view_template(&)
9
- div(class: "flex flex-row gap-3 items-center") do
10
- input(
11
- type: "hidden",
12
- value: "0",
13
- autocomplete: "off",
14
- **@kwargs
15
- )
16
- input(
17
- type: "checkbox",
18
- value: "1",
19
- class: "appearance-none size-4 border border-zinc-300 rounded hover:border-zinc-500 hover:cursor-pointer checked:bg-blue-700",
20
- **@kwargs
21
- )
22
- render RUI::Forms::Label.new(&) if block_given?
23
- end
24
- end
25
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class RUI::Forms::Email < RUI::Base
4
- def initialize(**kwargs)
5
- @kwargs = kwargs
6
- end
7
-
8
- def view_template(&)
9
- render RUI::Forms::FieldGroup.new do
10
- render RUI::Forms::Label.new(&) if block_given?
11
- input(
12
- type: "email",
13
- class: "border border-zinc-300 hover:border-zinc-400 rounded px-2 py-1",
14
- **@kwargs
15
- )
16
- end
17
- div
18
- end
19
- end
@@ -1,19 +0,0 @@
1
- module RUI::Forms::Helpers
2
- DEFAULT_CLASSES = "flex flex-col gap-4".freeze
3
-
4
- def rui_form_with(**options, &block)
5
- if options.present? && options[:html][:class].present?
6
- form_with_merged_classes(options[:html][:class], **options, &block)
7
- else
8
- # TODO: Remove reference to builder
9
- form_with(**options.merge(builder: TailwindFormBuilder, html: { class: DEFAULT_CLASSES }), &block)
10
- end
11
- end
12
-
13
- private
14
-
15
- def form_with_merged_classes(provided_classes, **options, &block)
16
- css = RUI::TailwindMerger.instance.merge(DEFAULT_CLASSES, provided_classes)
17
- form_with(**options.merge(html: { class: css }), &block)
18
- end
19
- end
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class RUI::Forms::Label < RUI::Base
4
- def view_template(&)
5
- label(class: "text-sm font-semibold", &)
6
- end
7
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class RUI::Forms::Password < RUI::Base
4
- def initialize(**kwargs)
5
- @kwargs = kwargs
6
- end
7
-
8
- def view_template(&)
9
- render RUI::Forms::FieldGroup.new do
10
- render RUI::Forms::Label.new(&) if block_given?
11
- input(
12
- type: "password",
13
- class: "border border-zinc-300 hover:border-zinc-400 rounded px-2 py-1",
14
- **@kwargs
15
- )
16
- end
17
- end
18
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class RUI::Forms::Phone < RUI::Base
4
- def initialize(**kwargs)
5
- @kwargs = kwargs
6
- end
7
-
8
- def view_template(&)
9
- render RUI::Forms::FieldGroup.new do
10
- render RUI::Forms::Label.new(&) if block_given?
11
- input(
12
- type: :tel,
13
- class: "border border-zinc-300 hover:border-zinc-400 rounded px-2 py-1",
14
- **@kwargs
15
- )
16
- end
17
- end
18
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class RUI::Forms::Radio < RUI::Base
4
- def initialize(label:, &)
5
- @label = label
6
- super(&)
7
- end
8
-
9
- def view_template
10
- render RUI::Forms::FieldGroup.new do
11
- render RUI::Forms::Label.new { @label }
12
- yield
13
- end
14
- end
15
-
16
- def option(**attrs, &)
17
- div(class: "flex flex-row gap-3 items-center") do
18
- input(class: "hover:cursor-pointer", type: :radio, **attrs)
19
- label(&)
20
- end
21
- end
22
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class RUI::Forms::Select < RUI::Base
4
- def initialize(label:, **attrs, &)
5
- @label = label
6
- @attrs = attrs
7
- super(&)
8
- end
9
-
10
- def view_template(&)
11
- render RUI::Forms::FieldGroup.new do
12
- render RUI::Forms::Label.new { @label }
13
- select(
14
- class: "border border-zinc-300 hover:border-zinc-400 rounded px-2 py-1.5 hover:cursor-pointer",
15
- **@attrs,
16
- &
17
- )
18
- end
19
- end
20
-
21
- def option(**attrs, &)
22
- tag(:option, **attrs, &)
23
- end
24
- end
@@ -1,142 +0,0 @@
1
- module RUI::Forms
2
- class TailwindFormBuilder < ActionView::Helpers::FormBuilder
3
- include ActionView::Helpers::TagHelper
4
-
5
- class_attribute :text_field_helpers, default: field_helpers - [ :label, :check_box, :radio_button, :fields_for, :fields, :hidden_field, :file_field ]
6
-
7
- TEXT_FIELD_STYLE = "bg-white ring ring-zinc-100 hover:ring-zinc-400 rounded px-2 py-1".freeze
8
- SELECT_FIELD_STYLE = "block bg-white ring ring-zinc-100 hover:ring-zinc-400 rounded px-2 py-1".freeze
9
-
10
- text_field_helpers.each do |field_method|
11
- class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
12
- def #{field_method}(method, options = {})
13
- if options.delete(:tailwindified)
14
- super
15
- else
16
- text_like_field(#{field_method.inspect}, method, options)
17
- end
18
- end
19
- RUBY_EVAL
20
- end
21
-
22
- def submit(value = nil, options = {})
23
- custom_opts, opts = partition_custom_opts(options)
24
- style_classes = RUI::TailwindMerger.instance.merge(
25
- RUI::Buttons::Base::STYLE,
26
- RUI::Buttons::Primary::STYLE
27
- )
28
- classes = apply_style_classes(style_classes, custom_opts)
29
-
30
- @template.content_tag("div", super(value, { class: classes }.merge(opts)))
31
- end
32
-
33
- def select(method, choices = nil, options = {}, html_options = {}, &block)
34
- custom_opts, opts = partition_custom_opts(options)
35
- classes = apply_style_classes(SELECT_FIELD_STYLE, custom_opts, method)
36
-
37
- labels = labels(method, custom_opts[:label], options)
38
- field = super(method, choices, opts, html_options.merge({ class: classes }), &block)
39
-
40
- labels + field
41
- end
42
-
43
- def collection_check_boxes(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
44
- custom_opts = partition_custom_opts(options)
45
-
46
- check_boxes = @template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options), &block)
47
-
48
- labels = labels(method, custom_opts, options)
49
-
50
- @template.content_tag("div", labels + check_boxes, { class: "flex flex-col gap-3 items-start justify-middle" })
51
- end
52
-
53
- # def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
54
- # custom_opts = partition_custom_opts(options)
55
-
56
- # buttons = @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options), &block)
57
-
58
- # labels = labels(method, custom_opts, options)
59
-
60
- # @template.content_tag("div", labels + buttons, { class: "flex flex-col gap-3 items-start justify-middle" })
61
- # end
62
-
63
- def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
64
- @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options))
65
- end
66
-
67
- private
68
-
69
- def text_like_field(field_method, object_method, options = {})
70
- custom_opts, opts = partition_custom_opts(options)
71
-
72
- classes = apply_style_classes(TEXT_FIELD_STYLE, custom_opts, object_method)
73
-
74
- field = send(field_method, object_method, {
75
- class: classes,
76
- title: errors_for(object_method)&.join(" ")
77
- }.compact.merge(opts).merge({ tailwindified: true }))
78
-
79
- labels = labels(object_method, custom_opts[:label], options)
80
-
81
- @template.content_tag("div", labels + field, { class: "flex flex-col gap-1" })
82
- end
83
-
84
- def labels(object_method, label_options, field_options)
85
- label = tailwind_label(object_method, label_options, field_options)
86
- error_label = error_label(object_method, field_options)
87
-
88
- @template.content_tag("div", label + error_label, { class: "flex flex-col items-start" })
89
- end
90
-
91
- def tailwind_label(object_method, label_options, field_options)
92
- text, label_opts = if label_options.present?
93
- [ label_options[:text], label_options.except(:text) ]
94
- else
95
- [ nil, {} ]
96
- end
97
-
98
- label_classes = label_opts[:class] || "text-sm font-semibold"
99
- label_classes += " font-zinc-500" if field_options[:disabled]
100
- label(object_method, text, {
101
- class: label_classes
102
- }.merge(label_opts.except(:class)))
103
- end
104
-
105
- def error_label(object_method, options)
106
- if errors_for(object_method).present?
107
- error_message = @object.errors[object_method].collect(&:titleize).join(", ")
108
- tailwind_label(object_method, { text: error_message, class: " font-bold text-red-500" }, options)
109
- end
110
- end
111
-
112
- def border_color_classes(object_method)
113
- if errors_for(object_method).present?
114
- " border-2 border-red-400 focus:border-rose-200"
115
- else
116
- " border border-gray-300 focus:border-yellow-700"
117
- end
118
- end
119
-
120
- def apply_style_classes(classes, custom_opts, object_method = nil)
121
- classes + border_color_classes(object_method) + " #{custom_opts[:class]}"
122
- end
123
-
124
- CUSTOM_OPTS = [ :label, :class ].freeze
125
- def partition_custom_opts(opts)
126
- opts.partition { |k, v| CUSTOM_OPTS.include?(k) }.map(&:to_h)
127
- end
128
-
129
- def errors_for(object_method)
130
- return unless @object.present? && object_method.present?
131
-
132
- @object.errors[object_method]
133
- end
134
-
135
- def check_box_classes(method, field_classes = nil)
136
- classes = <<~CLASSES.strip
137
- block rounded size-3.5 focus:ring focus:ring-success checked:bg-success checked:hover:bg-success/90 cursor-pointer focus:ring-opacity-50
138
- CLASSES
139
- "#{classes} #{field_classes} #{border_color_classes(method)}"
140
- end
141
- end
142
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class RUI::Forms::Text < RUI::Base
4
- def initialize(**kwargs)
5
- @kwargs = kwargs
6
- end
7
-
8
- def view_template(&)
9
- render RUI::Forms::FieldGroup.new do
10
- render RUI::Forms::Label.new(&) if block_given?
11
- input(
12
- class: "border border-zinc-300 hover:border-zinc-400 rounded px-2 py-1",
13
- **@kwargs
14
- )
15
- end
16
- end
17
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class RUI::Forms::Textarea < RUI::Base
4
- def initialize(**kwargs)
5
- @kwargs = kwargs
6
- end
7
-
8
- def view_template(&)
9
- render RUI::Forms::FieldGroup.new do
10
- render RUI::Forms::Label.new(&) if block_given?
11
- textarea(
12
- rows: 5,
13
- class: "border border-zinc-300 hover:border-zinc-400 rounded px-2 py-1",
14
- **@kwargs
15
- )
16
- end
17
- end
18
- end