better_ui 0.6.0 → 0.7.1

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 (198) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +257 -212
  3. data/Rakefile +11 -2
  4. data/app/components/better_ui/action_messages_component/action_messages_component.html.erb +48 -0
  5. data/app/components/better_ui/action_messages_component.rb +544 -0
  6. data/app/components/better_ui/application_component.rb +66 -0
  7. data/app/components/better_ui/button_component/button_component.html.erb +31 -0
  8. data/app/components/better_ui/button_component.rb +307 -0
  9. data/app/components/better_ui/card_component/card_component.html.erb +17 -0
  10. data/app/components/better_ui/card_component.rb +460 -0
  11. data/app/components/better_ui/drawer/header_component/header_component.html.erb +24 -0
  12. data/app/components/better_ui/drawer/header_component.rb +238 -0
  13. data/app/components/better_ui/drawer/layout_component/layout_component.html.erb +44 -0
  14. data/app/components/better_ui/drawer/layout_component.rb +270 -0
  15. data/app/components/better_ui/drawer/nav_group_component/nav_group_component.html.erb +10 -0
  16. data/app/components/better_ui/drawer/nav_group_component.rb +155 -0
  17. data/app/components/better_ui/drawer/nav_item_component/nav_item_component.html.erb +13 -0
  18. data/app/components/better_ui/drawer/nav_item_component.rb +225 -0
  19. data/app/components/better_ui/drawer/sidebar_component/sidebar_component.html.erb +17 -0
  20. data/app/components/better_ui/drawer/sidebar_component.rb +263 -0
  21. data/app/components/better_ui/forms/base_component.rb +450 -0
  22. data/app/components/better_ui/forms/checkbox_component/checkbox_component.html.erb +28 -0
  23. data/app/components/better_ui/forms/checkbox_component.rb +419 -0
  24. data/app/components/better_ui/forms/checkbox_group_component/checkbox_group_component.html.erb +40 -0
  25. data/app/components/better_ui/forms/checkbox_group_component.rb +363 -0
  26. data/app/components/better_ui/forms/number_input_component/number_input_component.html.erb +40 -0
  27. data/app/components/better_ui/forms/number_input_component.rb +320 -0
  28. data/app/components/better_ui/forms/password_input_component/password_input_component.html.erb +71 -0
  29. data/app/components/better_ui/forms/password_input_component.rb +206 -0
  30. data/app/components/better_ui/forms/text_input_component/text_input_component.html.erb +40 -0
  31. data/app/components/better_ui/forms/text_input_component.rb +258 -0
  32. data/app/components/better_ui/forms/textarea_component/textarea_component.html.erb +40 -0
  33. data/app/components/better_ui/forms/textarea_component.rb +329 -0
  34. data/app/form_builders/better_ui/ui_form_builder.rb +467 -0
  35. data/app/helpers/better_ui/application_helper.rb +325 -58
  36. data/app/views/layouts/better_ui/application.html.erb +1 -1
  37. data/config/routes.rb +1 -0
  38. data/lib/better_ui/engine.rb +34 -5
  39. data/lib/better_ui/version.rb +1 -1
  40. data/lib/better_ui.rb +32 -5
  41. data/lib/generators/better_ui/install/USAGE +44 -0
  42. data/lib/generators/better_ui/install/install_generator.rb +87 -0
  43. data/lib/generators/better_ui/install/templates/better_ui_theme.css.tt +280 -0
  44. data/lib/tasks/better_ui_tasks.rake +39 -4
  45. metadata +55 -203
  46. data/app/components/better_ui/application/card/component.html.erb +0 -20
  47. data/app/components/better_ui/application/card/component.rb +0 -214
  48. data/app/components/better_ui/application/main/component.html.erb +0 -9
  49. data/app/components/better_ui/application/main/component.rb +0 -123
  50. data/app/components/better_ui/application/navbar/component.html.erb +0 -92
  51. data/app/components/better_ui/application/navbar/component.rb +0 -136
  52. data/app/components/better_ui/application/sidebar/component.html.erb +0 -249
  53. data/app/components/better_ui/application/sidebar/component.rb +0 -187
  54. data/app/components/better_ui/general/accordion/component.html.erb +0 -5
  55. data/app/components/better_ui/general/accordion/component.rb +0 -92
  56. data/app/components/better_ui/general/accordion/item_component.html.erb +0 -12
  57. data/app/components/better_ui/general/accordion/item_component.rb +0 -176
  58. data/app/components/better_ui/general/alert/component.html.erb +0 -32
  59. data/app/components/better_ui/general/alert/component.rb +0 -242
  60. data/app/components/better_ui/general/avatar/component.html.erb +0 -20
  61. data/app/components/better_ui/general/avatar/component.rb +0 -301
  62. data/app/components/better_ui/general/badge/component.html.erb +0 -23
  63. data/app/components/better_ui/general/badge/component.rb +0 -248
  64. data/app/components/better_ui/general/breadcrumb/component.html.erb +0 -15
  65. data/app/components/better_ui/general/breadcrumb/component.rb +0 -187
  66. data/app/components/better_ui/general/button/component.html.erb +0 -34
  67. data/app/components/better_ui/general/button/component.rb +0 -214
  68. data/app/components/better_ui/general/divider/component.html.erb +0 -10
  69. data/app/components/better_ui/general/divider/component.rb +0 -226
  70. data/app/components/better_ui/general/dropdown/component.html.erb +0 -28
  71. data/app/components/better_ui/general/dropdown/component.rb +0 -192
  72. data/app/components/better_ui/general/dropdown/divider_component.html.erb +0 -1
  73. data/app/components/better_ui/general/dropdown/divider_component.rb +0 -41
  74. data/app/components/better_ui/general/dropdown/item_component.html.erb +0 -6
  75. data/app/components/better_ui/general/dropdown/item_component.rb +0 -119
  76. data/app/components/better_ui/general/field/component.html.erb +0 -27
  77. data/app/components/better_ui/general/field/component.rb +0 -37
  78. data/app/components/better_ui/general/grid/cell_component.html.erb +0 -3
  79. data/app/components/better_ui/general/grid/cell_component.rb +0 -390
  80. data/app/components/better_ui/general/grid/component.html.erb +0 -3
  81. data/app/components/better_ui/general/grid/component.rb +0 -301
  82. data/app/components/better_ui/general/heading/component.html.erb +0 -22
  83. data/app/components/better_ui/general/heading/component.rb +0 -257
  84. data/app/components/better_ui/general/icon/component.html.erb +0 -7
  85. data/app/components/better_ui/general/icon/component.rb +0 -240
  86. data/app/components/better_ui/general/input/checkbox/component.html.erb +0 -5
  87. data/app/components/better_ui/general/input/checkbox/component.rb +0 -238
  88. data/app/components/better_ui/general/input/datetime/component.html.erb +0 -5
  89. data/app/components/better_ui/general/input/datetime/component.rb +0 -223
  90. data/app/components/better_ui/general/input/pin/component.html.erb +0 -1
  91. data/app/components/better_ui/general/input/pin/component.rb +0 -201
  92. data/app/components/better_ui/general/input/radio/component.html.erb +0 -5
  93. data/app/components/better_ui/general/input/radio/component.rb +0 -230
  94. data/app/components/better_ui/general/input/rating/component.html.erb +0 -4
  95. data/app/components/better_ui/general/input/rating/component.rb +0 -272
  96. data/app/components/better_ui/general/input/select/component.html.erb +0 -78
  97. data/app/components/better_ui/general/input/select/component.rb +0 -249
  98. data/app/components/better_ui/general/input/select/select_component.html.erb +0 -5
  99. data/app/components/better_ui/general/input/select/select_component.rb +0 -37
  100. data/app/components/better_ui/general/input/text/component.html.erb +0 -5
  101. data/app/components/better_ui/general/input/text/component.rb +0 -171
  102. data/app/components/better_ui/general/input/textarea/component.html.erb +0 -5
  103. data/app/components/better_ui/general/input/textarea/component.rb +0 -166
  104. data/app/components/better_ui/general/input/toggle/component.html.erb +0 -5
  105. data/app/components/better_ui/general/input/toggle/component.rb +0 -242
  106. data/app/components/better_ui/general/link/component.html.erb +0 -18
  107. data/app/components/better_ui/general/link/component.rb +0 -258
  108. data/app/components/better_ui/general/modal/component.html.erb +0 -5
  109. data/app/components/better_ui/general/modal/component.rb +0 -47
  110. data/app/components/better_ui/general/modal/modal_component.html.erb +0 -52
  111. data/app/components/better_ui/general/modal/modal_component.rb +0 -160
  112. data/app/components/better_ui/general/pagination/component.html.erb +0 -85
  113. data/app/components/better_ui/general/pagination/component.rb +0 -216
  114. data/app/components/better_ui/general/panel/component.html.erb +0 -28
  115. data/app/components/better_ui/general/panel/component.rb +0 -249
  116. data/app/components/better_ui/general/progress/component.html.erb +0 -11
  117. data/app/components/better_ui/general/progress/component.rb +0 -160
  118. data/app/components/better_ui/general/spinner/component.html.erb +0 -35
  119. data/app/components/better_ui/general/spinner/component.rb +0 -93
  120. data/app/components/better_ui/general/table/component.html.erb +0 -5
  121. data/app/components/better_ui/general/table/component.rb +0 -217
  122. data/app/components/better_ui/general/table/tbody_component.html.erb +0 -3
  123. data/app/components/better_ui/general/table/tbody_component.rb +0 -30
  124. data/app/components/better_ui/general/table/td_component.html.erb +0 -3
  125. data/app/components/better_ui/general/table/td_component.rb +0 -44
  126. data/app/components/better_ui/general/table/tfoot_component.html.erb +0 -3
  127. data/app/components/better_ui/general/table/tfoot_component.rb +0 -28
  128. data/app/components/better_ui/general/table/th_component.html.erb +0 -6
  129. data/app/components/better_ui/general/table/th_component.rb +0 -51
  130. data/app/components/better_ui/general/table/thead_component.html.erb +0 -3
  131. data/app/components/better_ui/general/table/thead_component.rb +0 -28
  132. data/app/components/better_ui/general/table/tr_component.html.erb +0 -3
  133. data/app/components/better_ui/general/table/tr_component.rb +0 -30
  134. data/app/components/better_ui/general/tabs/component.html.erb +0 -11
  135. data/app/components/better_ui/general/tabs/component.rb +0 -120
  136. data/app/components/better_ui/general/tabs/panel_component.html.erb +0 -3
  137. data/app/components/better_ui/general/tabs/panel_component.rb +0 -37
  138. data/app/components/better_ui/general/tabs/tab_component.html.erb +0 -13
  139. data/app/components/better_ui/general/tabs/tab_component.rb +0 -111
  140. data/app/components/better_ui/general/tag/component.html.erb +0 -3
  141. data/app/components/better_ui/general/tag/component.rb +0 -104
  142. data/app/components/better_ui/general/text/component.html.erb +0 -1
  143. data/app/components/better_ui/general/text/component.rb +0 -194
  144. data/app/components/better_ui/general/tooltip/component.html.erb +0 -7
  145. data/app/components/better_ui/general/tooltip/component.rb +0 -239
  146. data/app/helpers/better_ui/application/components/card/card_helper.rb +0 -96
  147. data/app/helpers/better_ui/application/components/card.rb +0 -11
  148. data/app/helpers/better_ui/application/components/main/main_helper.rb +0 -64
  149. data/app/helpers/better_ui/application/components/navbar/navbar_helper.rb +0 -77
  150. data/app/helpers/better_ui/application/components/sidebar/sidebar_helper.rb +0 -51
  151. data/app/helpers/better_ui/general/components/accordion/accordion_helper.rb +0 -73
  152. data/app/helpers/better_ui/general/components/alert/alert_helper.rb +0 -57
  153. data/app/helpers/better_ui/general/components/avatar/avatar_helper.rb +0 -29
  154. data/app/helpers/better_ui/general/components/badge/badge_helper.rb +0 -53
  155. data/app/helpers/better_ui/general/components/breadcrumb/breadcrumb_helper.rb +0 -37
  156. data/app/helpers/better_ui/general/components/button/button_helper.rb +0 -65
  157. data/app/helpers/better_ui/general/components/container/container_helper.rb +0 -60
  158. data/app/helpers/better_ui/general/components/divider/divider_helper.rb +0 -63
  159. data/app/helpers/better_ui/general/components/dropdown/divider_helper.rb +0 -32
  160. data/app/helpers/better_ui/general/components/dropdown/dropdown_helper.rb +0 -88
  161. data/app/helpers/better_ui/general/components/dropdown/item_helper.rb +0 -68
  162. data/app/helpers/better_ui/general/components/field/field_helper.rb +0 -26
  163. data/app/helpers/better_ui/general/components/grid/grid_helper.rb +0 -145
  164. data/app/helpers/better_ui/general/components/heading/heading_helper.rb +0 -72
  165. data/app/helpers/better_ui/general/components/icon/icon_helper.rb +0 -16
  166. data/app/helpers/better_ui/general/components/input/checkbox/checkbox_helper.rb +0 -81
  167. data/app/helpers/better_ui/general/components/input/datetime/datetime_helper.rb +0 -91
  168. data/app/helpers/better_ui/general/components/input/pin/pin_helper.rb +0 -76
  169. data/app/helpers/better_ui/general/components/input/radio/radio_helper.rb +0 -79
  170. data/app/helpers/better_ui/general/components/input/radio_group/radio_group_helper.rb +0 -124
  171. data/app/helpers/better_ui/general/components/input/rating/rating_helper.rb +0 -70
  172. data/app/helpers/better_ui/general/components/input/select/select_helper.rb +0 -86
  173. data/app/helpers/better_ui/general/components/input/text/text_helper.rb +0 -138
  174. data/app/helpers/better_ui/general/components/input/textarea/textarea_helper.rb +0 -73
  175. data/app/helpers/better_ui/general/components/input/toggle/toggle_helper.rb +0 -77
  176. data/app/helpers/better_ui/general/components/link/link_helper.rb +0 -89
  177. data/app/helpers/better_ui/general/components/modal/modal_helper.rb +0 -85
  178. data/app/helpers/better_ui/general/components/pagination/pagination_helper.rb +0 -82
  179. data/app/helpers/better_ui/general/components/panel/panel_helper.rb +0 -83
  180. data/app/helpers/better_ui/general/components/progress/progress_helper.rb +0 -53
  181. data/app/helpers/better_ui/general/components/spinner/spinner_helper.rb +0 -19
  182. data/app/helpers/better_ui/general/components/table/table_helper.rb +0 -53
  183. data/app/helpers/better_ui/general/components/table/tbody_helper.rb +0 -13
  184. data/app/helpers/better_ui/general/components/table/td_helper.rb +0 -19
  185. data/app/helpers/better_ui/general/components/table/tfoot_helper.rb +0 -13
  186. data/app/helpers/better_ui/general/components/table/th_helper.rb +0 -19
  187. data/app/helpers/better_ui/general/components/table/thead_helper.rb +0 -13
  188. data/app/helpers/better_ui/general/components/table/tr_helper.rb +0 -13
  189. data/app/helpers/better_ui/general/components/tabs/panel_helper.rb +0 -62
  190. data/app/helpers/better_ui/general/components/tabs/tab_helper.rb +0 -55
  191. data/app/helpers/better_ui/general/components/tabs/tabs_helper.rb +0 -95
  192. data/app/helpers/better_ui/general/components/tag/tag_helper.rb +0 -26
  193. data/app/helpers/better_ui/general/components/text/text_helper.rb +0 -83
  194. data/app/helpers/better_ui/general/components/tooltip/tooltip_helper.rb +0 -60
  195. data/app/jobs/better_ui/application_job.rb +0 -4
  196. data/app/mailers/better_ui/application_mailer.rb +0 -6
  197. data/config/initializers/lookbook.rb +0 -23
  198. data/lib/better_ui/railtie.rb +0 -20
@@ -0,0 +1,544 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ # ActionMessagesComponent displays a list of messages with customizable styles and variants.
5
+ #
6
+ # This component is designed for displaying form validation errors, flash notifications,
7
+ # success messages, warnings, and other user feedback. It supports multiple visual styles
8
+ # (solid, soft, outline, ghost), all semantic color variants, optional titles, dismissible
9
+ # functionality, and auto-dismiss timers.
10
+ #
11
+ # Features:
12
+ # - 9 color variants (primary, secondary, accent, success, danger, warning, info, light, dark)
13
+ # - 4 visual styles (solid, soft, outline, ghost)
14
+ # - Optional title/heading
15
+ # - Dismissible with close button
16
+ # - Auto-dismiss after N seconds
17
+ # - Stimulus controller integration for interactivity
18
+ # - Icon slot for custom icons (via ViewComponent slots)
19
+ #
20
+ # @example Basic usage
21
+ # <%= render BetterUi::ActionMessagesComponent.new(messages: ["This is a message"]) %>
22
+ #
23
+ # @example Form validation errors
24
+ # <%= render BetterUi::ActionMessagesComponent.new(
25
+ # variant: :danger,
26
+ # title: "Please correct the following errors:",
27
+ # messages: @user.errors.full_messages
28
+ # ) %>
29
+ #
30
+ # @example Success notification with auto-dismiss
31
+ # <%= render BetterUi::ActionMessagesComponent.new(
32
+ # variant: :success,
33
+ # style: :solid,
34
+ # dismissible: true,
35
+ # auto_dismiss: 5,
36
+ # messages: ["Your changes have been saved."]
37
+ # ) %>
38
+ #
39
+ # @example With custom styling
40
+ # <%= render BetterUi::ActionMessagesComponent.new(
41
+ # variant: :warning,
42
+ # style: :outline,
43
+ # title: "Warning",
44
+ # messages: ["This action cannot be undone"],
45
+ # container_classes: "shadow-lg"
46
+ # ) %>
47
+ #
48
+ # @see ApplicationComponent
49
+ class ActionMessagesComponent < ApplicationComponent
50
+ attr_reader :messages, :variant, :style, :dismissible, :auto_dismiss, :title, :container_classes
51
+
52
+ # Initialize the ActionMessages component
53
+ #
54
+ # @param messages [Array<String>] List of messages to display
55
+ # @param variant [Symbol] Color variant (:primary, :secondary, :accent, :success, :danger, :warning, :info, :light, :dark)
56
+ # @param style [Symbol] Visual style (:solid, :soft, :outline, :ghost)
57
+ # @param dismissible [Boolean] Whether to show a dismiss button
58
+ # @param auto_dismiss [Integer, Float, nil] Auto-dismiss after N seconds (nil to disable)
59
+ # @param title [String, nil] Optional title/heading above messages
60
+ # @param container_classes [String, nil] Custom CSS classes to merge with container
61
+ # @param options [Hash] Additional HTML attributes (id, data, aria, etc.)
62
+ def initialize(
63
+ messages: [],
64
+ variant: :info,
65
+ style: :soft,
66
+ dismissible: false,
67
+ auto_dismiss: nil,
68
+ title: nil,
69
+ container_classes: nil,
70
+ **options
71
+ )
72
+ # Convert messages to array (handles nil and single strings)
73
+ @messages = Array(messages)
74
+ @variant = variant.to_sym
75
+ @style = style.to_sym
76
+ @dismissible = dismissible
77
+ @auto_dismiss = auto_dismiss
78
+ @title = title
79
+ @container_classes = container_classes
80
+ @options = options
81
+
82
+ # Validate parameters on initialization
83
+ validate_variant!
84
+ validate_style!
85
+ end
86
+
87
+ # Build CSS classes for the main container
88
+ # Combines base classes (rounded, padding) with style-specific classes and custom classes
89
+ # @return [String] Merged CSS class string
90
+ def component_classes
91
+ base_classes = [ "rounded-lg", "p-4", "relative" ]
92
+ style_classes = send("#{@style}_classes") # Dynamically call solid_classes, soft_classes, etc.
93
+
94
+ # Use TailwindMerge to intelligently merge classes (handles conflicts)
95
+ css_classes(base_classes, style_classes, @container_classes)
96
+ end
97
+
98
+ # Build HTML attributes for the component
99
+ # Adds Stimulus controller and auto-dismiss data attribute if configured
100
+ # @return [Hash] HTML attributes hash
101
+ def component_attributes
102
+ attrs = @options.dup
103
+ attrs[:data] ||= {}
104
+ # Register Stimulus controller for dismiss/auto-dismiss functionality
105
+ attrs[:data][:controller] = "better-ui--action-messages"
106
+
107
+ # Add auto-dismiss value if configured (Stimulus will read this)
108
+ if @auto_dismiss.present? && @auto_dismiss.to_f > 0
109
+ attrs[:data][:"better-ui--action-messages-auto-dismiss-value"] = @auto_dismiss.to_f
110
+ end
111
+
112
+ attrs
113
+ end
114
+
115
+ # CSS classes for the title element
116
+ # @return [String] Merged CSS class string
117
+ def title_classes
118
+ css_classes("font-semibold", "mb-2", title_color_classes)
119
+ end
120
+
121
+ # CSS classes for the content wrapper (contains icon slot and message list)
122
+ # @return [String] Merged CSS class string
123
+ def content_wrapper_classes
124
+ css_classes("flex", "gap-3")
125
+ end
126
+
127
+ # CSS classes for the message list (<ul>)
128
+ # @return [String] Merged CSS class string
129
+ def list_classes
130
+ css_classes("list-none", "list-inside", "space-y-1", "flex-1")
131
+ end
132
+
133
+ # CSS classes for individual message list items (<li>)
134
+ # @return [String] Merged CSS class string
135
+ def list_item_classes
136
+ css_classes("text-sm")
137
+ end
138
+
139
+ # CSS classes for the dismiss button
140
+ # @return [String] Merged CSS class string
141
+ def dismiss_button_classes
142
+ button_base = [ "absolute", "top-3", "right-3", "p-1", "rounded", "transition-colors" ]
143
+ button_colors = dismiss_button_color_classes
144
+
145
+ css_classes(button_base, button_colors)
146
+ end
147
+
148
+ private
149
+
150
+ # Validates that the variant is one of the allowed variants.
151
+ #
152
+ # @raise [ArgumentError] if variant is not in ApplicationComponent::VARIANTS
153
+ # @api private
154
+ def validate_variant!
155
+ unless BetterUi::ApplicationComponent::VARIANTS.key?(@variant)
156
+ raise ArgumentError, "Invalid variant: #{@variant}. Must be one of #{BetterUi::ApplicationComponent::VARIANTS.keys.join(', ')}"
157
+ end
158
+ end
159
+
160
+ # Validates that the style is one of the allowed styles.
161
+ #
162
+ # @raise [ArgumentError] if style is not valid (solid, soft, outline, ghost)
163
+ # @api private
164
+ def validate_style!
165
+ valid_styles = [ :solid, :soft, :outline, :ghost ]
166
+ unless valid_styles.include?(@style)
167
+ raise ArgumentError, "Invalid style: #{@style}. Must be one of #{valid_styles.join(', ')}"
168
+ end
169
+ end
170
+
171
+ # ============================================================================
172
+ # STYLE METHODS
173
+ # ============================================================================
174
+ # Each style method returns hardcoded CSS class strings for Tailwind JIT.
175
+ # Dynamic class generation (e.g., "bg-#{variant}-600") breaks Tailwind's
176
+ # static scanner, so we use case statements with explicit class strings.
177
+ # ============================================================================
178
+
179
+ # Solid style: full color background with white/dark text.
180
+ #
181
+ # Best for: Bold, high-contrast messages
182
+ # @return [Array<String>] array of CSS classes for solid style
183
+ # @api private
184
+ def solid_classes
185
+ bg_classes = case @variant
186
+ when :primary
187
+ [ "bg-primary-600" ]
188
+ when :secondary
189
+ [ "bg-secondary-500" ]
190
+ when :accent
191
+ [ "bg-accent-500" ]
192
+ when :success
193
+ [ "bg-success-600" ]
194
+ when :danger
195
+ [ "bg-danger-600" ]
196
+ when :warning
197
+ [ "bg-warning-500" ]
198
+ when :info
199
+ [ "bg-info-500" ]
200
+ when :light
201
+ [ "bg-grayscale-100" ]
202
+ when :dark
203
+ [ "bg-grayscale-900" ]
204
+ end
205
+
206
+ text_classes = solid_text_color_classes
207
+
208
+ bg_classes + text_classes
209
+ end
210
+
211
+ # Soft style: light background (shade 50) with colored text and subtle border.
212
+ #
213
+ # Best for: Gentle, non-intrusive messages (default style)
214
+ # @return [Array<String>] array of CSS classes for soft style
215
+ # @api private
216
+ def soft_classes
217
+ bg_classes = case @variant
218
+ when :primary
219
+ [ "bg-primary-50" ]
220
+ when :secondary
221
+ [ "bg-secondary-50" ]
222
+ when :accent
223
+ [ "bg-accent-50" ]
224
+ when :success
225
+ [ "bg-success-50" ]
226
+ when :danger
227
+ [ "bg-danger-50" ]
228
+ when :warning
229
+ [ "bg-warning-50" ]
230
+ when :info
231
+ [ "bg-info-50" ]
232
+ when :light
233
+ [ "bg-grayscale-50" ]
234
+ when :dark
235
+ [ "bg-grayscale-100" ]
236
+ end
237
+
238
+ border_classes = case @variant
239
+ when :primary
240
+ [ "border", "border-primary-200" ]
241
+ when :secondary
242
+ [ "border", "border-secondary-200" ]
243
+ when :accent
244
+ [ "border", "border-accent-200" ]
245
+ when :success
246
+ [ "border", "border-success-200" ]
247
+ when :danger
248
+ [ "border", "border-danger-200" ]
249
+ when :warning
250
+ [ "border", "border-warning-200" ]
251
+ when :info
252
+ [ "border", "border-info-200" ]
253
+ when :light
254
+ [ "border", "border-grayscale-200" ]
255
+ when :dark
256
+ [ "border", "border-grayscale-300" ]
257
+ end
258
+
259
+ text_classes = soft_text_color_classes
260
+
261
+ bg_classes + border_classes + text_classes
262
+ end
263
+
264
+ # Outline style: white background with thick colored border.
265
+ #
266
+ # Best for: Clean, professional look with emphasis on border
267
+ # @return [Array<String>] array of CSS classes for outline style
268
+ # @api private
269
+ def outline_classes
270
+ border_classes = case @variant
271
+ when :primary
272
+ [ "border-2", "border-primary-500", "bg-white" ]
273
+ when :secondary
274
+ [ "border-2", "border-secondary-500", "bg-white" ]
275
+ when :accent
276
+ [ "border-2", "border-accent-500", "bg-white" ]
277
+ when :success
278
+ [ "border-2", "border-success-500", "bg-white" ]
279
+ when :danger
280
+ [ "border-2", "border-danger-500", "bg-white" ]
281
+ when :warning
282
+ [ "border-2", "border-warning-500", "bg-white" ]
283
+ when :info
284
+ [ "border-2", "border-info-500", "bg-white" ]
285
+ when :light
286
+ [ "border-2", "border-grayscale-300", "bg-white" ]
287
+ when :dark
288
+ [ "border-2", "border-grayscale-700", "bg-white" ]
289
+ end
290
+
291
+ text_classes = outline_text_color_classes
292
+
293
+ border_classes + text_classes
294
+ end
295
+
296
+ # Ghost style: transparent background with colored text and hover effect.
297
+ #
298
+ # Best for: Subtle, minimal messages that blend with the page
299
+ # @return [Array<String>] array of CSS classes for ghost style
300
+ # @api private
301
+ def ghost_classes
302
+ text_classes = ghost_text_color_classes
303
+ hover_classes = ghost_hover_classes
304
+
305
+ text_classes + hover_classes
306
+ end
307
+
308
+ # ============================================================================
309
+ # TEXT COLOR HELPERS
310
+ # ============================================================================
311
+ # These methods return text colors appropriate for each style.
312
+ # ============================================================================
313
+
314
+ # Text colors for solid style backgrounds.
315
+ #
316
+ # @return [Array<String>] array of text color classes for solid style
317
+ # @api private
318
+ def solid_text_color_classes
319
+ case @variant
320
+ when :light
321
+ [ "text-grayscale-900" ]
322
+ when :dark
323
+ [ "text-white" ]
324
+ when :warning
325
+ [ "text-grayscale-900" ]
326
+ else
327
+ [ "text-white" ]
328
+ end
329
+ end
330
+
331
+ # Text colors for soft style backgrounds.
332
+ #
333
+ # @return [Array<String>] array of text color classes for soft style
334
+ # @api private
335
+ def soft_text_color_classes
336
+ case @variant
337
+ when :primary
338
+ [ "text-primary-900" ]
339
+ when :secondary
340
+ [ "text-secondary-900" ]
341
+ when :accent
342
+ [ "text-accent-900" ]
343
+ when :success
344
+ [ "text-success-900" ]
345
+ when :danger
346
+ [ "text-danger-900" ]
347
+ when :warning
348
+ [ "text-warning-900" ]
349
+ when :info
350
+ [ "text-info-900" ]
351
+ when :light
352
+ [ "text-grayscale-900" ]
353
+ when :dark
354
+ [ "text-grayscale-900" ]
355
+ end
356
+ end
357
+
358
+ # Text colors for outline style.
359
+ #
360
+ # @return [Array<String>] array of text color classes for outline style
361
+ # @api private
362
+ def outline_text_color_classes
363
+ case @variant
364
+ when :primary
365
+ [ "text-primary-700" ]
366
+ when :secondary
367
+ [ "text-secondary-700" ]
368
+ when :accent
369
+ [ "text-accent-700" ]
370
+ when :success
371
+ [ "text-success-700" ]
372
+ when :danger
373
+ [ "text-danger-700" ]
374
+ when :warning
375
+ [ "text-warning-700" ]
376
+ when :info
377
+ [ "text-info-700" ]
378
+ when :light
379
+ [ "text-grayscale-700" ]
380
+ when :dark
381
+ [ "text-grayscale-900" ]
382
+ end
383
+ end
384
+
385
+ # Text colors for ghost style.
386
+ #
387
+ # @return [Array<String>] array of text color classes for ghost style
388
+ # @api private
389
+ def ghost_text_color_classes
390
+ case @variant
391
+ when :primary
392
+ [ "text-primary-600" ]
393
+ when :secondary
394
+ [ "text-secondary-600" ]
395
+ when :accent
396
+ [ "text-accent-600" ]
397
+ when :success
398
+ [ "text-success-600" ]
399
+ when :danger
400
+ [ "text-danger-600" ]
401
+ when :warning
402
+ [ "text-warning-600" ]
403
+ when :info
404
+ [ "text-info-600" ]
405
+ when :light
406
+ [ "text-grayscale-600" ]
407
+ when :dark
408
+ [ "text-grayscale-900" ]
409
+ end
410
+ end
411
+
412
+ # Hover background colors for ghost style.
413
+ #
414
+ # @return [Array<String>] array of hover background color classes
415
+ # @api private
416
+ def ghost_hover_classes
417
+ case @variant
418
+ when :primary
419
+ [ "hover:bg-primary-50" ]
420
+ when :secondary
421
+ [ "hover:bg-secondary-50" ]
422
+ when :accent
423
+ [ "hover:bg-accent-50" ]
424
+ when :success
425
+ [ "hover:bg-success-50" ]
426
+ when :danger
427
+ [ "hover:bg-danger-50" ]
428
+ when :warning
429
+ [ "hover:bg-warning-50" ]
430
+ when :info
431
+ [ "hover:bg-info-50" ]
432
+ when :light
433
+ [ "hover:bg-grayscale-50" ]
434
+ when :dark
435
+ [ "hover:bg-grayscale-100" ]
436
+ end
437
+ end
438
+
439
+ # Get appropriate text color for title based on current style.
440
+ #
441
+ # Delegates to the corresponding text color method for the active style.
442
+ #
443
+ # @return [Array<String>] array of text color classes for the title
444
+ # @api private
445
+ def title_color_classes
446
+ case @style
447
+ when :solid
448
+ solid_text_color_classes
449
+ when :soft
450
+ soft_text_color_classes
451
+ when :outline
452
+ outline_text_color_classes
453
+ when :ghost
454
+ ghost_text_color_classes
455
+ end
456
+ end
457
+
458
+ # Get appropriate colors for dismiss button based on style and variant.
459
+ #
460
+ # Button colors must provide good contrast and visibility for all style
461
+ # and variant combinations.
462
+ #
463
+ # @return [Array<String>] array of text and hover background color classes for dismiss button
464
+ # @api private
465
+ def dismiss_button_color_classes
466
+ case @style
467
+ when :solid
468
+ case @variant
469
+ when :light
470
+ [ "text-grayscale-700", "hover:bg-grayscale-200" ]
471
+ when :dark
472
+ [ "text-grayscale-300", "hover:bg-grayscale-800" ]
473
+ when :warning
474
+ [ "text-grayscale-700", "hover:bg-warning-600" ]
475
+ else
476
+ [ "text-white", "hover:bg-black", "hover:bg-opacity-10" ]
477
+ end
478
+ when :soft
479
+ case @variant
480
+ when :primary
481
+ [ "text-primary-700", "hover:bg-primary-100" ]
482
+ when :secondary
483
+ [ "text-secondary-700", "hover:bg-secondary-100" ]
484
+ when :accent
485
+ [ "text-accent-700", "hover:bg-accent-100" ]
486
+ when :success
487
+ [ "text-success-700", "hover:bg-success-100" ]
488
+ when :danger
489
+ [ "text-danger-700", "hover:bg-danger-100" ]
490
+ when :warning
491
+ [ "text-warning-700", "hover:bg-warning-100" ]
492
+ when :info
493
+ [ "text-info-700", "hover:bg-info-100" ]
494
+ when :light
495
+ [ "text-grayscale-700", "hover:bg-grayscale-100" ]
496
+ when :dark
497
+ [ "text-grayscale-700", "hover:bg-grayscale-200" ]
498
+ end
499
+ when :outline
500
+ case @variant
501
+ when :primary
502
+ [ "text-primary-600", "hover:bg-primary-50" ]
503
+ when :secondary
504
+ [ "text-secondary-600", "hover:bg-secondary-50" ]
505
+ when :accent
506
+ [ "text-accent-600", "hover:bg-accent-50" ]
507
+ when :success
508
+ [ "text-success-600", "hover:bg-success-50" ]
509
+ when :danger
510
+ [ "text-danger-600", "hover:bg-danger-50" ]
511
+ when :warning
512
+ [ "text-warning-600", "hover:bg-warning-50" ]
513
+ when :info
514
+ [ "text-info-600", "hover:bg-info-50" ]
515
+ when :light
516
+ [ "text-grayscale-600", "hover:bg-grayscale-50" ]
517
+ when :dark
518
+ [ "text-grayscale-800", "hover:bg-grayscale-100" ]
519
+ end
520
+ when :ghost
521
+ case @variant
522
+ when :primary
523
+ [ "text-primary-600", "hover:bg-primary-100" ]
524
+ when :secondary
525
+ [ "text-secondary-600", "hover:bg-secondary-100" ]
526
+ when :accent
527
+ [ "text-accent-600", "hover:bg-accent-100" ]
528
+ when :success
529
+ [ "text-success-600", "hover:bg-success-100" ]
530
+ when :danger
531
+ [ "text-danger-600", "hover:bg-danger-100" ]
532
+ when :warning
533
+ [ "text-warning-600", "hover:bg-warning-100" ]
534
+ when :info
535
+ [ "text-info-600", "hover:bg-info-100" ]
536
+ when :light
537
+ [ "text-grayscale-600", "hover:bg-grayscale-100" ]
538
+ when :dark
539
+ [ "text-grayscale-800", "hover:bg-grayscale-200" ]
540
+ end
541
+ end
542
+ end
543
+ end
544
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ # Base component class for all ViewComponents in BetterUi.
5
+ #
6
+ # This class provides:
7
+ # - Common configuration for all components
8
+ # - Shared helper methods
9
+ # - Consistent behavior across components
10
+ #
11
+ # Usage:
12
+ # class BetterUi::MyComponent < BetterUi::ApplicationComponent
13
+ # # component implementation
14
+ # end
15
+ #
16
+ # Best Practices:
17
+ # - Use composition over inheritance
18
+ # - Keep instance methods private
19
+ # - Pass data explicitly (avoid global state)
20
+ # - Test against rendered content, not internals
21
+ class ApplicationComponent < ViewComponent::Base
22
+ # Enable content areas (slots) by default
23
+ # Allows components to accept flexible content blocks
24
+ # Example: <%= render(BetterUi::MyComponent.new) do |c| %>
25
+ # <% c.with_header { "Title" } %>
26
+ # <% end %>
27
+
28
+ # Color variant definitions with default shades
29
+ #
30
+ # This is the single source of truth for all color variants used throughout BetterUi.
31
+ # Use VARIANTS.keys to iterate over available variants in components and preview templates.
32
+ #
33
+ # Related CSS definitions:
34
+ # - CSS custom properties defined in host app via generator (better_ui_theme.css @theme inline)
35
+ # - Typography color utilities: .text-heading-{variant} (@layer utilities)
36
+ #
37
+ # Note: Case statements in components must use hardcoded Tailwind class strings
38
+ # (e.g., "bg-primary-600") for the JIT compiler to detect them at build time.
39
+ # The VARIANTS constant is used for iteration and validation only.
40
+ VARIANTS = {
41
+ primary: 600, # Strong, trustworthy actions
42
+ secondary: 500, # Neutral, supporting elements
43
+ accent: 500, # Highlights and special features
44
+ success: 600, # Positive actions, confirmations
45
+ danger: 600, # Destructive actions, errors
46
+ warning: 500, # Caution, alerts
47
+ info: 500, # Informational, tips
48
+ light: 100, # Light backgrounds and light text
49
+ dark: 900 # Dark backgrounds and dark text
50
+ }.freeze
51
+
52
+ private
53
+
54
+ # Helper to merge CSS classes intelligently using TailwindMerge
55
+ # Resolves conflicting Tailwind utility classes
56
+ #
57
+ # @param classes [Array<String>] CSS class names to merge
58
+ # @return [String] Merged CSS classes
59
+ #
60
+ # Example:
61
+ # css_classes("px-4 py-2", "px-6") #=> "py-2 px-6"
62
+ def css_classes(*classes)
63
+ TailwindMerge::Merger.new.merge(classes.compact.join(" "))
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,31 @@
1
+ <% if link? %>
2
+ <a <%= tag.attributes(component_attributes) %> class="<%= component_classes %>">
3
+ <span data-better-ui--button-target="spinner" class="<%= show_loader ? '' : 'hidden' %>">
4
+ <svg class="animate-spin <%= SIZES[size][:icon] %>" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
5
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
6
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
7
+ </svg>
8
+ <span class="sr-only">Loading...</span>
9
+ </span>
10
+ <span data-better-ui--button-target="content" class="<%= show_loader ? 'hidden' : '' %> inline-flex items-center <%= SIZES[size][:gap] %>">
11
+ <%= icon_before if icon_before? %>
12
+ <%= content %>
13
+ <%= icon_after if icon_after? %>
14
+ </span>
15
+ </a>
16
+ <% else %>
17
+ <button <%= tag.attributes(component_attributes) %> class="<%= component_classes %>">
18
+ <span data-better-ui--button-target="spinner" class="<%= show_loader ? '' : 'hidden' %>">
19
+ <svg class="animate-spin <%= SIZES[size][:icon] %>" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
20
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
21
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
22
+ </svg>
23
+ <span class="sr-only">Loading...</span>
24
+ </span>
25
+ <span data-better-ui--button-target="content" class="<%= show_loader ? 'hidden' : '' %> inline-flex items-center <%= SIZES[size][:gap] %>">
26
+ <%= icon_before if icon_before? %>
27
+ <%= content %>
28
+ <%= icon_after if icon_after? %>
29
+ </span>
30
+ </button>
31
+ <% end %>