better_ui 0.7.2 → 0.9.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +86 -29
  3. data/app/components/better_ui/application_component.rb +34 -0
  4. data/app/components/better_ui/avatar_component/avatar_component.html.erb +15 -0
  5. data/app/components/better_ui/avatar_component.rb +227 -0
  6. data/app/components/better_ui/badge_component/badge_component.html.erb +16 -0
  7. data/app/components/better_ui/badge_component.rb +114 -0
  8. data/app/components/better_ui/breadcrumb/breadcrumb_component/breadcrumb_component.html.erb +12 -0
  9. data/app/components/better_ui/breadcrumb/breadcrumb_component.rb +148 -0
  10. data/app/components/better_ui/breadcrumb/item_component/item_component.html.erb +11 -0
  11. data/app/components/better_ui/breadcrumb/item_component.rb +78 -0
  12. data/app/components/better_ui/card_component.rb +45 -11
  13. data/app/components/better_ui/concerns/inline_label_styles.rb +137 -0
  14. data/app/components/better_ui/container_component/container_component.html.erb +3 -0
  15. data/app/components/better_ui/container_component.rb +143 -0
  16. data/app/components/better_ui/dialog/alert_component/alert_component.html.erb +61 -0
  17. data/app/components/better_ui/dialog/alert_component.rb +78 -0
  18. data/app/components/better_ui/dialog/confirm_component/confirm_component.html.erb +67 -0
  19. data/app/components/better_ui/dialog/confirm_component.rb +80 -0
  20. data/app/components/better_ui/dialog/dialog_component/dialog_component.html.erb +44 -0
  21. data/app/components/better_ui/dialog/dialog_component.rb +81 -0
  22. data/app/components/better_ui/divider_component/divider_component.html.erb +11 -0
  23. data/app/components/better_ui/divider_component.rb +344 -0
  24. data/app/components/better_ui/drawer/sidebar_component.rb +1 -0
  25. data/app/components/better_ui/dropdown/divider_component/divider_component.html.erb +1 -0
  26. data/app/components/better_ui/dropdown/divider_component.rb +20 -0
  27. data/app/components/better_ui/dropdown/dropdown_component/dropdown_component.html.erb +19 -0
  28. data/app/components/better_ui/dropdown/dropdown_component.rb +108 -0
  29. data/app/components/better_ui/dropdown/header_component/header_component.html.erb +3 -0
  30. data/app/components/better_ui/dropdown/header_component.rb +25 -0
  31. data/app/components/better_ui/dropdown/item_component/item_component.html.erb +7 -0
  32. data/app/components/better_ui/dropdown/item_component.rb +97 -0
  33. data/app/components/better_ui/fa_icon_component/fa_icon_component.html.erb +1 -0
  34. data/app/components/better_ui/fa_icon_component.rb +165 -0
  35. data/app/components/better_ui/forms/base_component.rb +3 -1
  36. data/app/components/better_ui/forms/select_component/select_component.html.erb +86 -0
  37. data/app/components/better_ui/forms/select_component.rb +347 -0
  38. data/app/components/better_ui/forms/text_input_component.rb +24 -2
  39. data/app/components/better_ui/heading_component/heading_component.html.erb +11 -0
  40. data/app/components/better_ui/heading_component.rb +259 -0
  41. data/app/components/better_ui/link_component/link_component.html.erb +5 -0
  42. data/app/components/better_ui/link_component.rb +169 -0
  43. data/app/components/better_ui/progress_component/progress_component.html.erb +15 -0
  44. data/app/components/better_ui/progress_component.rb +98 -0
  45. data/app/components/better_ui/spinner_component/spinner_component.html.erb +11 -0
  46. data/app/components/better_ui/spinner_component.rb +70 -0
  47. data/app/components/better_ui/table/cell_component/cell_component.html.erb +3 -0
  48. data/app/components/better_ui/table/cell_component.rb +84 -0
  49. data/app/components/better_ui/table/column_component.rb +75 -0
  50. data/app/components/better_ui/table/header_cell_component/header_cell_component.html.erb +18 -0
  51. data/app/components/better_ui/table/header_cell_component.rb +138 -0
  52. data/app/components/better_ui/table/header_component/header_component.html.erb +5 -0
  53. data/app/components/better_ui/table/header_component.rb +37 -0
  54. data/app/components/better_ui/table/row_component/row_component.html.erb +5 -0
  55. data/app/components/better_ui/table/row_component.rb +88 -0
  56. data/app/components/better_ui/table/table_component/table_component.html.erb +90 -0
  57. data/app/components/better_ui/table/table_component.rb +467 -0
  58. data/app/components/better_ui/tabs/container_component/container_component.html.erb +40 -0
  59. data/app/components/better_ui/tabs/container_component.rb +428 -0
  60. data/app/components/better_ui/tabs/panel_component/panel_component.html.erb +3 -0
  61. data/app/components/better_ui/tabs/panel_component.rb +105 -0
  62. data/app/components/better_ui/tabs/tab_component/tab_component.html.erb +9 -0
  63. data/app/components/better_ui/tabs/tab_component.rb +316 -0
  64. data/app/components/better_ui/tag_component/tag_component.html.erb +33 -0
  65. data/app/components/better_ui/tag_component.rb +114 -0
  66. data/app/components/better_ui/tooltip_component/tooltip_component.html.erb +11 -0
  67. data/app/components/better_ui/tooltip_component.rb +154 -0
  68. data/app/form_builders/better_ui/ui_form_builder.rb +90 -0
  69. data/app/helpers/better_ui/application_helper.rb +575 -0
  70. data/lib/better_ui/engine.rb +7 -0
  71. data/lib/better_ui/version.rb +1 -1
  72. metadata +63 -3
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module Breadcrumb
5
+ # A breadcrumb navigation component that renders an ordered list of items
6
+ # with configurable separators and sizes.
7
+ #
8
+ # This is a compound component that uses ItemComponent for individual breadcrumb items.
9
+ # The last item (without href) is typically rendered as the current page indicator.
10
+ #
11
+ # @example Basic breadcrumb
12
+ # <%= render BetterUi::Breadcrumb::BreadcrumbComponent.new do |breadcrumb| %>
13
+ # <% breadcrumb.with_item(label: "Home", href: "/") %>
14
+ # <% breadcrumb.with_item(label: "Products", href: "/products") %>
15
+ # <% breadcrumb.with_item(label: "Widget") %>
16
+ # <% end %>
17
+ #
18
+ # @example With chevron separator and large size
19
+ # <%= render BetterUi::Breadcrumb::BreadcrumbComponent.new(separator: :chevron, size: :lg) do |breadcrumb| %>
20
+ # <% breadcrumb.with_item(label: "Home", href: "/") %>
21
+ # <% breadcrumb.with_item(label: "Settings") %>
22
+ # <% end %>
23
+ class BreadcrumbComponent < ApplicationComponent
24
+ # @!method with_item
25
+ # Slot for rendering breadcrumb items.
26
+ # @param label [String] the item label text
27
+ # @param href [String, nil] the item link URL
28
+ # @yieldparam [BetterUi::Breadcrumb::ItemComponent] item the item component instance
29
+ # @yieldreturn [String] the HTML content for the item
30
+ renders_many :items, BetterUi::Breadcrumb::ItemComponent
31
+
32
+ # Allowed separator types
33
+ SEPARATORS = %i[slash chevron dot].freeze
34
+
35
+ # Allowed size options
36
+ SIZES = %i[sm md lg].freeze
37
+
38
+ # Initializes a new breadcrumb component.
39
+ #
40
+ # @param separator [Symbol] the separator type (:slash, :chevron, :dot), defaults to :slash
41
+ # @param size [Symbol] the text size (:sm, :md, :lg), defaults to :md
42
+ # @param container_classes [String, nil] additional CSS classes for the nav element
43
+ # @param options [Hash] additional HTML attributes passed to the nav element
44
+ #
45
+ # @raise [ArgumentError] if separator is not one of the allowed values
46
+ # @raise [ArgumentError] if size is not one of the allowed values
47
+ def initialize(
48
+ separator: :slash,
49
+ size: :md,
50
+ container_classes: nil,
51
+ **options
52
+ )
53
+ @separator = validate_separator(separator)
54
+ @size = validate_size(size)
55
+ @container_classes = container_classes
56
+ @options = options
57
+ end
58
+
59
+ private
60
+
61
+ # Returns the complete CSS classes for the ordered list element.
62
+ #
63
+ # @return [String] the merged CSS class string
64
+ # @api private
65
+ def component_classes
66
+ css_classes(
67
+ "flex",
68
+ "items-center",
69
+ "flex-wrap",
70
+ "gap-1",
71
+ size_classes
72
+ )
73
+ end
74
+
75
+ # Returns CSS classes for separator elements.
76
+ #
77
+ # @return [String] the merged CSS class string
78
+ # @api private
79
+ def separator_classes
80
+ css_classes(
81
+ "mx-2",
82
+ "text-grayscale-400"
83
+ )
84
+ end
85
+
86
+ # Returns the separator content (character or SVG) based on the separator type.
87
+ #
88
+ # @return [String] the separator HTML content
89
+ # @api private
90
+ def separator_content
91
+ case @separator
92
+ when :slash
93
+ "/"
94
+ when :chevron
95
+ chevron_svg
96
+ when :dot
97
+ "\u00B7"
98
+ end
99
+ end
100
+
101
+ # Returns the size-specific CSS class.
102
+ #
103
+ # @return [String] the Tailwind text size class
104
+ # @api private
105
+ def size_classes
106
+ case @size
107
+ when :sm then "text-sm"
108
+ when :md then "text-base"
109
+ when :lg then "text-lg"
110
+ end
111
+ end
112
+
113
+ # Returns an SVG chevron-right icon for the chevron separator.
114
+ #
115
+ # @return [String] the SVG HTML markup
116
+ # @api private
117
+ def chevron_svg
118
+ '<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" /></svg>'.html_safe
119
+ end
120
+
121
+ # Validates the separator parameter.
122
+ #
123
+ # @param separator [Symbol] the separator to validate
124
+ # @return [Symbol] the validated separator
125
+ # @raise [ArgumentError] if separator is invalid
126
+ # @api private
127
+ def validate_separator(separator)
128
+ unless SEPARATORS.include?(separator)
129
+ raise ArgumentError, "Invalid separator: #{separator}. Must be one of: #{SEPARATORS.join(', ')}"
130
+ end
131
+ separator
132
+ end
133
+
134
+ # Validates the size parameter.
135
+ #
136
+ # @param size [Symbol] the size to validate
137
+ # @return [Symbol] the validated size
138
+ # @raise [ArgumentError] if size is invalid
139
+ # @api private
140
+ def validate_size(size)
141
+ unless SIZES.include?(size)
142
+ raise ArgumentError, "Invalid size: #{size}. Must be one of: #{SIZES.join(', ')}"
143
+ end
144
+ size
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,11 @@
1
+ <% if current? %>
2
+ <span class="<%= current_classes %>" aria-current="page">
3
+ <%= icon_before if icon_before? %>
4
+ <%= @label %>
5
+ </span>
6
+ <% else %>
7
+ <a href="<%= @href %>" class="<%= link_classes %>">
8
+ <%= icon_before if icon_before? %>
9
+ <%= @label %>
10
+ </a>
11
+ <% end %>
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module Breadcrumb
5
+ # A breadcrumb item component that renders as a link or current page indicator.
6
+ #
7
+ # When an href is provided, the item renders as a link. When href is nil,
8
+ # it renders as a span with aria-current="page" to indicate the current page.
9
+ #
10
+ # @example Link item
11
+ # <%= render BetterUi::Breadcrumb::ItemComponent.new(label: "Home", href: "/") %>
12
+ #
13
+ # @example Current page item (no href)
14
+ # <%= render BetterUi::Breadcrumb::ItemComponent.new(label: "Settings") %>
15
+ #
16
+ # @example With icon
17
+ # <%= render BetterUi::Breadcrumb::ItemComponent.new(label: "Home", href: "/") do |item| %>
18
+ # <% item.with_icon_before do %>
19
+ # <svg>...</svg>
20
+ # <% end %>
21
+ # <% end %>
22
+ class ItemComponent < ApplicationComponent
23
+ # @!method with_icon_before
24
+ # Slot for rendering an icon before the label.
25
+ # @yieldreturn [String] the SVG or icon HTML content
26
+ renders_one :icon_before
27
+
28
+ # Initializes a new breadcrumb item component.
29
+ #
30
+ # @param label [String] the text to display (required)
31
+ # @param href [String, nil] the link URL (nil renders as current page)
32
+ # @param options [Hash] additional HTML attributes passed to the element
33
+ def initialize(label:, href: nil, **options)
34
+ @label = label
35
+ @href = href
36
+ @options = options
37
+ end
38
+
39
+ # Whether this item represents the current page.
40
+ #
41
+ # @return [Boolean] true if no href is provided
42
+ def current?
43
+ @href.nil?
44
+ end
45
+
46
+ private
47
+
48
+ # Returns CSS classes for the current page indicator.
49
+ #
50
+ # @return [String] the merged CSS class string
51
+ # @api private
52
+ def current_classes
53
+ css_classes(
54
+ "text-grayscale-500",
55
+ "font-medium",
56
+ "inline-flex",
57
+ "items-center",
58
+ "gap-1"
59
+ )
60
+ end
61
+
62
+ # Returns CSS classes for the link element.
63
+ #
64
+ # @return [String] the merged CSS class string
65
+ # @api private
66
+ def link_classes
67
+ css_classes(
68
+ "text-grayscale-600",
69
+ "hover:text-grayscale-900",
70
+ "inline-flex",
71
+ "items-center",
72
+ "gap-1",
73
+ "transition-colors"
74
+ )
75
+ end
76
+ end
77
+ end
78
+ end
@@ -43,7 +43,7 @@ module BetterUi
43
43
  # # Soft: Light colored background
44
44
  # <%= render BetterUi::CardComponent.new(style: :soft) { "Soft card" } %>
45
45
  #
46
- # # Bordered: Neutral gray border (variant-agnostic)
46
+ # # Bordered: White background with variant-colored border (defaults to :light)
47
47
  # <%= render BetterUi::CardComponent.new(style: :bordered) { "Bordered card" } %>
48
48
  class CardComponent < ApplicationComponent
49
49
  # Size configurations for padding, text, and border radius
@@ -78,7 +78,8 @@ module BetterUi
78
78
 
79
79
  # Initializes a new card component.
80
80
  #
81
- # @param variant [Symbol] the color variant (:primary, :secondary, :accent, :success, :danger, :warning, :info, :light, :dark), defaults to :primary
81
+ # @param variant [Symbol, nil] the color variant (:primary, :secondary, :accent, :success, :danger, :warning, :info, :light, :dark).
82
+ # Defaults to :primary for most styles, :light for :bordered style.
82
83
  # @param style [Symbol] the visual style (:solid, :outline, :ghost, :soft, :bordered), defaults to :solid
83
84
  # @param size [Symbol] the size variant (:xs, :sm, :md, :lg, :xl), defaults to :md
84
85
  # @param shadow [Boolean] whether to apply shadow, defaults to true
@@ -127,10 +128,10 @@ module BetterUi
127
128
  # <% end %>
128
129
  # <% end %>
129
130
  def initialize(
130
- variant: :primary,
131
+ variant: nil,
131
132
  style: :solid,
132
133
  size: :md,
133
- shadow: true,
134
+ shadow: :sm,
134
135
  header_padding: true,
135
136
  body_padding: true,
136
137
  footer_padding: true,
@@ -140,10 +141,11 @@ module BetterUi
140
141
  footer_classes: nil,
141
142
  **options
142
143
  )
143
- @variant = validate_variant(variant)
144
144
  @style = validate_style(style)
145
+ resolved_variant = variant || default_variant_for_style
146
+ @variant = validate_variant(resolved_variant)
145
147
  @size = validate_size(size)
146
- @shadow = shadow
148
+ @shadow = normalize_shadow(shadow)
147
149
  @header_padding = header_padding
148
150
  @body_padding = body_padding
149
151
  @footer_padding = footer_padding
@@ -299,13 +301,23 @@ module BetterUi
299
301
  end
300
302
 
301
303
  # Returns CSS classes for bordered style.
302
- # Neutral white background with gray border, variant-agnostic.
303
- # Perfect for visual content separation and isolation.
304
+ # White background with variant-colored border.
305
+ # Positioned between soft and outline in visual weight.
304
306
  #
305
307
  # @return [Array<String>] array of CSS classes
306
308
  # @api private
307
309
  def bordered_classes
308
- [ "bg-white", "border", "border-gray-300", "text-gray-900" ]
310
+ case @variant
311
+ when :primary then [ "bg-white", "border", "border-primary-300", "text-grayscale-900" ]
312
+ when :secondary then [ "bg-white", "border", "border-secondary-300", "text-grayscale-900" ]
313
+ when :accent then [ "bg-white", "border", "border-accent-300", "text-grayscale-900" ]
314
+ when :success then [ "bg-white", "border", "border-success-300", "text-grayscale-900" ]
315
+ when :danger then [ "bg-white", "border", "border-danger-300", "text-grayscale-900" ]
316
+ when :warning then [ "bg-white", "border", "border-warning-300", "text-grayscale-900" ]
317
+ when :info then [ "bg-white", "border", "border-info-300", "text-grayscale-900" ]
318
+ when :light then [ "bg-white", "border", "border-grayscale-300", "text-grayscale-900" ]
319
+ when :dark then [ "bg-white", "border", "border-grayscale-700", "text-grayscale-900" ]
320
+ end
309
321
  end
310
322
 
311
323
  # Returns shadow CSS classes based on the shadow parameter.
@@ -313,7 +325,7 @@ module BetterUi
313
325
  # @return [String, nil] shadow class or nil
314
326
  # @api private
315
327
  def shadow_classes
316
- @shadow ? "shadow-md" : nil
328
+ SHADOWS[@shadow]
317
329
  end
318
330
 
319
331
  # Returns the size configuration hash for the current size.
@@ -406,7 +418,17 @@ module BetterUi
406
418
  when :ghost
407
419
  "border-transparent"
408
420
  when :bordered
409
- "border-gray-300"
421
+ case @variant
422
+ when :primary then "border-primary-200"
423
+ when :secondary then "border-secondary-200"
424
+ when :accent then "border-accent-200"
425
+ when :success then "border-success-200"
426
+ when :danger then "border-danger-200"
427
+ when :warning then "border-warning-200"
428
+ when :info then "border-info-200"
429
+ when :light then "border-grayscale-200"
430
+ when :dark then "border-grayscale-600"
431
+ end
410
432
  end
411
433
  end
412
434
 
@@ -418,6 +440,18 @@ module BetterUi
418
440
  @options
419
441
  end
420
442
 
443
+ # Returns the default variant for the current style.
444
+ # Bordered style defaults to :light (neutral gray), all others default to :primary.
445
+ #
446
+ # @return [Symbol] the default variant
447
+ # @api private
448
+ def default_variant_for_style
449
+ case @style
450
+ when :bordered then :light
451
+ else :primary
452
+ end
453
+ end
454
+
421
455
  # Validates the variant parameter.
422
456
  #
423
457
  # @param variant [Symbol] the variant to validate
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module Concerns
5
+ # Shared styling methods for inline label components (Badge, Tag).
6
+ #
7
+ # Provides variant-aware color classes for solid, outline, soft, and ghost styles.
8
+ # Components including this concern must define their own STYLES constant and
9
+ # may override hook methods for style divergence.
10
+ #
11
+ # Hook methods:
12
+ # - outline_light_text_class: CSS class for light variant outline text
13
+ # (default: "text-grayscale-400")
14
+ module InlineLabelStyles
15
+ extend ActiveSupport::Concern
16
+
17
+ private
18
+
19
+ def solid_classes
20
+ bg_classes = case @variant
21
+ when :primary
22
+ [ "bg-primary-600" ]
23
+ when :secondary
24
+ [ "bg-secondary-600" ]
25
+ when :accent
26
+ [ "bg-accent-600" ]
27
+ when :success
28
+ [ "bg-success-600" ]
29
+ when :danger
30
+ [ "bg-danger-600" ]
31
+ when :warning
32
+ [ "bg-warning-600" ]
33
+ when :info
34
+ [ "bg-info-600" ]
35
+ when :light
36
+ [ "bg-grayscale-100" ]
37
+ when :dark
38
+ [ "bg-grayscale-900" ]
39
+ end
40
+
41
+ bg_classes + [ text_color_for_solid ]
42
+ end
43
+
44
+ def outline_classes
45
+ color_classes = case @variant
46
+ when :primary
47
+ [ "border-primary-600", "text-primary-600" ]
48
+ when :secondary
49
+ [ "border-secondary-600", "text-secondary-600" ]
50
+ when :accent
51
+ [ "border-accent-600", "text-accent-600" ]
52
+ when :success
53
+ [ "border-success-600", "text-success-600" ]
54
+ when :danger
55
+ [ "border-danger-600", "text-danger-600" ]
56
+ when :warning
57
+ [ "border-warning-600", "text-warning-600" ]
58
+ when :info
59
+ [ "border-info-600", "text-info-600" ]
60
+ when :light
61
+ [ "border-grayscale-400", outline_light_text_class ]
62
+ when :dark
63
+ [ "border-grayscale-700", "text-grayscale-700" ]
64
+ end
65
+
66
+ [ "bg-transparent", "border" ] + color_classes
67
+ end
68
+
69
+ def soft_classes
70
+ case @variant
71
+ when :primary
72
+ [ "bg-primary-100", "text-primary-700" ]
73
+ when :secondary
74
+ [ "bg-secondary-100", "text-secondary-700" ]
75
+ when :accent
76
+ [ "bg-accent-100", "text-accent-700" ]
77
+ when :success
78
+ [ "bg-success-100", "text-success-700" ]
79
+ when :danger
80
+ [ "bg-danger-100", "text-danger-700" ]
81
+ when :warning
82
+ [ "bg-warning-100", "text-warning-700" ]
83
+ when :info
84
+ [ "bg-info-100", "text-info-700" ]
85
+ when :light
86
+ [ "bg-grayscale-100", "text-grayscale-700" ]
87
+ when :dark
88
+ [ "bg-grayscale-800", "text-grayscale-100" ]
89
+ end
90
+ end
91
+
92
+ def ghost_classes
93
+ color_classes = case @variant
94
+ when :primary
95
+ [ "text-primary-600" ]
96
+ when :secondary
97
+ [ "text-secondary-600" ]
98
+ when :accent
99
+ [ "text-accent-600" ]
100
+ when :success
101
+ [ "text-success-600" ]
102
+ when :danger
103
+ [ "text-danger-600" ]
104
+ when :warning
105
+ [ "text-warning-600" ]
106
+ when :info
107
+ [ "text-info-600" ]
108
+ when :light
109
+ [ "text-grayscale-700" ]
110
+ when :dark
111
+ [ "text-grayscale-700" ]
112
+ end
113
+
114
+ [ "bg-transparent" ] + color_classes
115
+ end
116
+
117
+ def text_color_for_solid
118
+ @variant == :light ? "text-grayscale-900" : "text-grayscale-50"
119
+ end
120
+
121
+ # Hook method for outline light variant text class.
122
+ # Override in including component to customize.
123
+ #
124
+ # @return [String] CSS class for outline light text color
125
+ def outline_light_text_class
126
+ "text-grayscale-400"
127
+ end
128
+
129
+ def validate_variant(variant)
130
+ unless BetterUi::ApplicationComponent::VARIANTS.key?(variant)
131
+ raise ArgumentError, "Invalid variant: #{variant}. Must be one of: #{BetterUi::ApplicationComponent::VARIANTS.keys.join(", ")}"
132
+ end
133
+ variant
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,3 @@
1
+ <div class="<%= component_classes %>" <%= tag.attributes(html_attributes) %>>
2
+ <%= content %>
3
+ </div>
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ # A responsive container component for constraining content width.
5
+ #
6
+ # This component provides a simple wrapper div with configurable max-width,
7
+ # horizontal padding, and centering. It is ideal for wrapping page content
8
+ # to maintain readable line lengths and consistent layout across breakpoints.
9
+ #
10
+ # @example Basic container (default: lg, padded, centered)
11
+ # <%= render BetterUi::ContainerComponent.new do %>
12
+ # Page content here
13
+ # <% end %>
14
+ #
15
+ # @example Extra-large container
16
+ # <%= render BetterUi::ContainerComponent.new(size: :xl) do %>
17
+ # Wide content area
18
+ # <% end %>
19
+ #
20
+ # @example Full-width container without padding
21
+ # <%= render BetterUi::ContainerComponent.new(size: :full, padding: false) do %>
22
+ # Edge-to-edge content
23
+ # <% end %>
24
+ #
25
+ # @example Non-centered container
26
+ # <%= render BetterUi::ContainerComponent.new(centered: false) do %>
27
+ # Left-aligned content
28
+ # <% end %>
29
+ class ContainerComponent < ApplicationComponent
30
+ # Available container sizes mapped to Tailwind max-width classes.
31
+ #
32
+ # @note Classes must be hardcoded strings for Tailwind JIT detection.
33
+ SIZES = {
34
+ sm: "max-w-screen-sm",
35
+ md: "max-w-screen-md",
36
+ lg: "max-w-screen-lg",
37
+ xl: "max-w-screen-xl",
38
+ full: "max-w-full"
39
+ }.freeze
40
+
41
+ # Initializes a new container component.
42
+ #
43
+ # @param size [Symbol] the max-width size (:sm, :md, :lg, :xl, :full), defaults to :lg
44
+ # @param padding [Boolean] whether to apply horizontal padding, defaults to true
45
+ # @param centered [Boolean] whether to center the container with mx-auto, defaults to true
46
+ # @param container_classes [String, nil] additional CSS classes for the container
47
+ # @param options [Hash] additional HTML attributes passed to the container div
48
+ #
49
+ # @raise [ArgumentError] if size is not one of the allowed values
50
+ #
51
+ # @example With all options
52
+ # <%= render BetterUi::ContainerComponent.new(
53
+ # size: :xl,
54
+ # padding: true,
55
+ # centered: true,
56
+ # container_classes: "my-8",
57
+ # id: "main-content",
58
+ # data: { controller: "page" }
59
+ # ) do %>
60
+ # Content here
61
+ # <% end %>
62
+ def initialize(
63
+ size: :lg,
64
+ padding: true,
65
+ centered: true,
66
+ container_classes: nil,
67
+ **options
68
+ )
69
+ @size = validate_size(size)
70
+ @padding = padding
71
+ @centered = centered
72
+ @container_classes = container_classes
73
+ @options = options
74
+ end
75
+
76
+ private
77
+
78
+ attr_reader :size, :padding, :centered, :container_classes, :options
79
+
80
+ # Returns the complete CSS classes for the container element.
81
+ #
82
+ # @return [String] the merged CSS class string
83
+ # @api private
84
+ def component_classes
85
+ css_classes([
86
+ "w-full",
87
+ size_classes,
88
+ padding_classes,
89
+ centered_classes,
90
+ @container_classes
91
+ ].flatten.compact)
92
+ end
93
+
94
+ # Returns the max-width class for the current size.
95
+ #
96
+ # @return [String] Tailwind max-width class
97
+ # @api private
98
+ def size_classes
99
+ SIZES[@size]
100
+ end
101
+
102
+ # Returns horizontal padding classes when padding is enabled.
103
+ #
104
+ # @return [String, nil] padding classes or nil
105
+ # @api private
106
+ def padding_classes
107
+ return nil unless @padding
108
+
109
+ "px-4 sm:px-6 lg:px-8"
110
+ end
111
+
112
+ # Returns centering class when centered is enabled.
113
+ #
114
+ # @return [String, nil] centering class or nil
115
+ # @api private
116
+ def centered_classes
117
+ return nil unless @centered
118
+
119
+ "mx-auto"
120
+ end
121
+
122
+ # Returns HTML attributes for the container element.
123
+ #
124
+ # @return [Hash] HTML attributes hash
125
+ # @api private
126
+ def html_attributes
127
+ @options
128
+ end
129
+
130
+ # Validates the size parameter.
131
+ #
132
+ # @param size [Symbol] the size to validate
133
+ # @return [Symbol] the validated size
134
+ # @raise [ArgumentError] if size is invalid
135
+ # @api private
136
+ def validate_size(size)
137
+ unless SIZES.key?(size)
138
+ raise ArgumentError, "Invalid size: #{size}. Must be one of: #{SIZES.keys.join(', ')}"
139
+ end
140
+ size
141
+ end
142
+ end
143
+ end