better_ui 0.9.0 → 0.10.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 (180) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -0
  3. data/app/components/better_ui/pagination/pagination_component/pagination_component.html.erb +137 -0
  4. data/app/components/better_ui/pagination/pagination_component.rb +408 -0
  5. data/app/components/better_ui/table/table_component/table_component.html.erb +2 -2
  6. data/app/components/better_ui/table/table_component.rb +30 -0
  7. data/app/helpers/better_ui/application_helper.rb +74 -0
  8. data/lib/better_ui/version.rb +1 -1
  9. data/spec/components/previews/better_ui/action_messages_component_preview/all_styles.html.erb +17 -0
  10. data/spec/components/previews/better_ui/action_messages_component_preview/all_variants.html.erb +19 -0
  11. data/spec/components/previews/better_ui/action_messages_component_preview/auto_dismiss.html.erb +51 -0
  12. data/spec/components/previews/better_ui/action_messages_component_preview/dismissible.html.erb +19 -0
  13. data/spec/components/previews/better_ui/action_messages_component_preview/with_title.html.erb +17 -0
  14. data/spec/components/previews/better_ui/action_messages_component_preview.rb +224 -0
  15. data/spec/components/previews/better_ui/avatar_component_preview/all_shapes.html.erb +26 -0
  16. data/spec/components/previews/better_ui/avatar_component_preview/all_sizes.html.erb +24 -0
  17. data/spec/components/previews/better_ui/avatar_component_preview/all_variants.html.erb +12 -0
  18. data/spec/components/previews/better_ui/avatar_component_preview/with_initials.html.erb +22 -0
  19. data/spec/components/previews/better_ui/avatar_component_preview/with_status.html.erb +26 -0
  20. data/spec/components/previews/better_ui/avatar_component_preview.rb +73 -0
  21. data/spec/components/previews/better_ui/badge_component_preview/all_sizes.html.erb +29 -0
  22. data/spec/components/previews/better_ui/badge_component_preview/all_styles.html.erb +26 -0
  23. data/spec/components/previews/better_ui/badge_component_preview/all_variants.html.erb +14 -0
  24. data/spec/components/previews/better_ui/badge_component_preview/counter_badges.html.erb +39 -0
  25. data/spec/components/previews/better_ui/badge_component_preview/dot_badges.html.erb +28 -0
  26. data/spec/components/previews/better_ui/badge_component_preview.rb +69 -0
  27. data/spec/components/previews/better_ui/breadcrumb/breadcrumb_component_preview/all_separators.html.erb +47 -0
  28. data/spec/components/previews/better_ui/breadcrumb/breadcrumb_component_preview/default.html.erb +23 -0
  29. data/spec/components/previews/better_ui/breadcrumb/breadcrumb_component_preview/with_icons.html.erb +43 -0
  30. data/spec/components/previews/better_ui/breadcrumb/breadcrumb_component_preview.rb +38 -0
  31. data/spec/components/previews/better_ui/button_component_preview/all_sizes.html.erb +25 -0
  32. data/spec/components/previews/better_ui/button_component_preview/all_variants.html.erb +14 -0
  33. data/spec/components/previews/better_ui/button_component_preview/as_links.html.erb +18 -0
  34. data/spec/components/previews/better_ui/button_component_preview/auto_loading_submit.html.erb +112 -0
  35. data/spec/components/previews/better_ui/button_component_preview/external_links.html.erb +61 -0
  36. data/spec/components/previews/better_ui/button_component_preview/form_integration.html.erb +102 -0
  37. data/spec/components/previews/better_ui/button_component_preview/interactive.html.erb +149 -0
  38. data/spec/components/previews/better_ui/button_component_preview/link_states.html.erb +36 -0
  39. data/spec/components/previews/better_ui/button_component_preview/loading_states.html.erb +62 -0
  40. data/spec/components/previews/better_ui/button_component_preview/turbo_method_links.html.erb +98 -0
  41. data/spec/components/previews/better_ui/button_component_preview/with_icons.html.erb +123 -0
  42. data/spec/components/previews/better_ui/button_component_preview.rb +155 -0
  43. data/spec/components/previews/better_ui/card_component_preview/all_sizes.html.erb +10 -0
  44. data/spec/components/previews/better_ui/card_component_preview/all_styles.html.erb +22 -0
  45. data/spec/components/previews/better_ui/card_component_preview/all_variants.html.erb +10 -0
  46. data/spec/components/previews/better_ui/card_component_preview.rb +269 -0
  47. data/spec/components/previews/better_ui/container_component_preview/all_sizes.html.erb +13 -0
  48. data/spec/components/previews/better_ui/container_component_preview.rb +59 -0
  49. data/spec/components/previews/better_ui/dialog/alert_component_preview/all_variants.html.erb +17 -0
  50. data/spec/components/previews/better_ui/dialog/alert_component_preview/custom_button_label.html.erb +14 -0
  51. data/spec/components/previews/better_ui/dialog/alert_component_preview/default.html.erb +13 -0
  52. data/spec/components/previews/better_ui/dialog/alert_component_preview/playground.html.erb +16 -0
  53. data/spec/components/previews/better_ui/dialog/alert_component_preview/without_icon.html.erb +14 -0
  54. data/spec/components/previews/better_ui/dialog/alert_component_preview.rb +57 -0
  55. data/spec/components/previews/better_ui/dialog/confirm_component_preview/all_variants.html.erb +17 -0
  56. data/spec/components/previews/better_ui/dialog/confirm_component_preview/custom_labels.html.erb +15 -0
  57. data/spec/components/previews/better_ui/dialog/confirm_component_preview/danger_confirm.html.erb +15 -0
  58. data/spec/components/previews/better_ui/dialog/confirm_component_preview/default.html.erb +13 -0
  59. data/spec/components/previews/better_ui/dialog/confirm_component_preview/playground.html.erb +17 -0
  60. data/spec/components/previews/better_ui/dialog/confirm_component_preview.rb +60 -0
  61. data/spec/components/previews/better_ui/dialog/dialog_component_preview/all_sizes.html.erb +32 -0
  62. data/spec/components/previews/better_ui/dialog/dialog_component_preview/default.html.erb +34 -0
  63. data/spec/components/previews/better_ui/dialog/dialog_component_preview/no_close_button.html.erb +28 -0
  64. data/spec/components/previews/better_ui/dialog/dialog_component_preview/playground.html.erb +39 -0
  65. data/spec/components/previews/better_ui/dialog/dialog_component_preview/with_all_slots.html.erb +52 -0
  66. data/spec/components/previews/better_ui/dialog/dialog_component_preview.rb +51 -0
  67. data/spec/components/previews/better_ui/divider_component_preview/all_styles.html.erb +58 -0
  68. data/spec/components/previews/better_ui/divider_component_preview/with_labels.html.erb +67 -0
  69. data/spec/components/previews/better_ui/divider_component_preview.rb +62 -0
  70. data/spec/components/previews/better_ui/drawer/header_component_preview.rb +169 -0
  71. data/spec/components/previews/better_ui/drawer/layout_component_preview/complete_layout.html.erb +87 -0
  72. data/spec/components/previews/better_ui/drawer/layout_component_preview/dark_theme.html.erb +36 -0
  73. data/spec/components/previews/better_ui/drawer/layout_component_preview/dashboard_example.html.erb +188 -0
  74. data/spec/components/previews/better_ui/drawer/layout_component_preview/default.html.erb +22 -0
  75. data/spec/components/previews/better_ui/drawer/layout_component_preview/primary_theme.html.erb +36 -0
  76. data/spec/components/previews/better_ui/drawer/layout_component_preview/right_sidebar.html.erb +44 -0
  77. data/spec/components/previews/better_ui/drawer/layout_component_preview/with_header_only.html.erb +20 -0
  78. data/spec/components/previews/better_ui/drawer/layout_component_preview/with_sidebar_only.html.erb +21 -0
  79. data/spec/components/previews/better_ui/drawer/layout_component_preview.rb +91 -0
  80. data/spec/components/previews/better_ui/drawer/nav_group_component_preview/complete_navigation.html.erb +55 -0
  81. data/spec/components/previews/better_ui/drawer/nav_group_component_preview.rb +163 -0
  82. data/spec/components/previews/better_ui/drawer/nav_item_component_preview.rb +104 -0
  83. data/spec/components/previews/better_ui/drawer/sidebar_component_preview.rb +212 -0
  84. data/spec/components/previews/better_ui/dropdown/dropdown_component_preview/all_sizes.html.erb +19 -0
  85. data/spec/components/previews/better_ui/dropdown/dropdown_component_preview/default.html.erb +12 -0
  86. data/spec/components/previews/better_ui/dropdown/dropdown_component_preview/disabled_items.html.erb +14 -0
  87. data/spec/components/previews/better_ui/dropdown/dropdown_component_preview/placement_options.html.erb +16 -0
  88. data/spec/components/previews/better_ui/dropdown/dropdown_component_preview/playground.html.erb +35 -0
  89. data/spec/components/previews/better_ui/dropdown/dropdown_component_preview/with_dividers_and_headers.html.erb +18 -0
  90. data/spec/components/previews/better_ui/dropdown/dropdown_component_preview/with_icons.html.erb +34 -0
  91. data/spec/components/previews/better_ui/dropdown/dropdown_component_preview.rb +59 -0
  92. data/spec/components/previews/better_ui/fa_icon_component_preview/all_sizes.html.erb +17 -0
  93. data/spec/components/previews/better_ui/fa_icon_component_preview/all_styles.html.erb +19 -0
  94. data/spec/components/previews/better_ui/fa_icon_component_preview/all_variants.html.erb +26 -0
  95. data/spec/components/previews/better_ui/fa_icon_component_preview/animations.html.erb +26 -0
  96. data/spec/components/previews/better_ui/fa_icon_component_preview/transformations.html.erb +88 -0
  97. data/spec/components/previews/better_ui/fa_icon_component_preview.rb +85 -0
  98. data/spec/components/previews/better_ui/forms/checkbox_component_preview/all_sizes.html.erb +12 -0
  99. data/spec/components/previews/better_ui/forms/checkbox_component_preview/all_variants.html.erb +12 -0
  100. data/spec/components/previews/better_ui/forms/checkbox_component_preview/form_integration.html.erb +32 -0
  101. data/spec/components/previews/better_ui/forms/checkbox_component_preview.rb +143 -0
  102. data/spec/components/previews/better_ui/forms/checkbox_group_component_preview/all_variants.html.erb +14 -0
  103. data/spec/components/previews/better_ui/forms/checkbox_group_component_preview/form_integration.html.erb +47 -0
  104. data/spec/components/previews/better_ui/forms/checkbox_group_component_preview/orientations.html.erb +34 -0
  105. data/spec/components/previews/better_ui/forms/checkbox_group_component_preview.rb +150 -0
  106. data/spec/components/previews/better_ui/forms/number_input_component_preview/all_sizes.html.erb +14 -0
  107. data/spec/components/previews/better_ui/forms/number_input_component_preview/form_integration.html.erb +45 -0
  108. data/spec/components/previews/better_ui/forms/number_input_component_preview.rb +211 -0
  109. data/spec/components/previews/better_ui/forms/password_input_component_preview/all_sizes.html.erb +12 -0
  110. data/spec/components/previews/better_ui/forms/password_input_component_preview/confirm_password_example.html.erb +29 -0
  111. data/spec/components/previews/better_ui/forms/password_input_component_preview/form_integration.html.erb +34 -0
  112. data/spec/components/previews/better_ui/forms/password_input_component_preview.rb +181 -0
  113. data/spec/components/previews/better_ui/forms/select_component_preview/all_sizes.html.erb +13 -0
  114. data/spec/components/previews/better_ui/forms/select_component_preview/all_states.html.erb +64 -0
  115. data/spec/components/previews/better_ui/forms/select_component_preview.rb +167 -0
  116. data/spec/components/previews/better_ui/forms/text_input_component_preview/all_sizes.html.erb +12 -0
  117. data/spec/components/previews/better_ui/forms/text_input_component_preview/all_types.html.erb +12 -0
  118. data/spec/components/previews/better_ui/forms/text_input_component_preview/form_integration.html.erb +33 -0
  119. data/spec/components/previews/better_ui/forms/text_input_component_preview.rb +247 -0
  120. data/spec/components/previews/better_ui/forms/textarea_component_preview/all_resize_variants.html.erb +13 -0
  121. data/spec/components/previews/better_ui/forms/textarea_component_preview/all_sizes.html.erb +12 -0
  122. data/spec/components/previews/better_ui/forms/textarea_component_preview/form_integration.html.erb +36 -0
  123. data/spec/components/previews/better_ui/forms/textarea_component_preview.rb +239 -0
  124. data/spec/components/previews/better_ui/heading_component_preview/all_alignments.html.erb +12 -0
  125. data/spec/components/previews/better_ui/heading_component_preview/all_levels.html.erb +7 -0
  126. data/spec/components/previews/better_ui/heading_component_preview/all_variants.html.erb +14 -0
  127. data/spec/components/previews/better_ui/heading_component_preview.rb +113 -0
  128. data/spec/components/previews/better_ui/link_component_preview/all_sizes.html.erb +25 -0
  129. data/spec/components/previews/better_ui/link_component_preview/all_styles.html.erb +14 -0
  130. data/spec/components/previews/better_ui/link_component_preview/all_variants.html.erb +14 -0
  131. data/spec/components/previews/better_ui/link_component_preview/with_icons.html.erb +66 -0
  132. data/spec/components/previews/better_ui/link_component_preview.rb +66 -0
  133. data/spec/components/previews/better_ui/pagination/pagination_component_preview/all_sizes.html.erb +13 -0
  134. data/spec/components/previews/better_ui/pagination/pagination_component_preview/all_styles.html.erb +13 -0
  135. data/spec/components/previews/better_ui/pagination/pagination_component_preview/all_variants.html.erb +13 -0
  136. data/spec/components/previews/better_ui/pagination/pagination_component_preview/default.html.erb +28 -0
  137. data/spec/components/previews/better_ui/pagination/pagination_component_preview/edge_cases.html.erb +82 -0
  138. data/spec/components/previews/better_ui/pagination/pagination_component_preview/with_info.html.erb +38 -0
  139. data/spec/components/previews/better_ui/pagination/pagination_component_preview.rb +88 -0
  140. data/spec/components/previews/better_ui/progress_component_preview/all_sizes.html.erb +15 -0
  141. data/spec/components/previews/better_ui/progress_component_preview/all_variants.html.erb +11 -0
  142. data/spec/components/previews/better_ui/progress_component_preview.rb +64 -0
  143. data/spec/components/previews/better_ui/spinner_component_preview/all_sizes.html.erb +17 -0
  144. data/spec/components/previews/better_ui/spinner_component_preview/all_variants.html.erb +11 -0
  145. data/spec/components/previews/better_ui/spinner_component_preview.rb +44 -0
  146. data/spec/components/previews/better_ui/table/table_component_preview/all_sizes.html.erb +28 -0
  147. data/spec/components/previews/better_ui/table/table_component_preview/all_variants.html.erb +34 -0
  148. data/spec/components/previews/better_ui/table/table_component_preview/bordered.html.erb +33 -0
  149. data/spec/components/previews/better_ui/table/table_component_preview/collection_mode.html.erb +31 -0
  150. data/spec/components/previews/better_ui/table/table_component_preview/default.html.erb +33 -0
  151. data/spec/components/previews/better_ui/table/table_component_preview/empty_state.html.erb +36 -0
  152. data/spec/components/previews/better_ui/table/table_component_preview/highlighted.html.erb +64 -0
  153. data/spec/components/previews/better_ui/table/table_component_preview/hoverable.html.erb +27 -0
  154. data/spec/components/previews/better_ui/table/table_component_preview/inside_card.html.erb +173 -0
  155. data/spec/components/previews/better_ui/table/table_component_preview/row_html.html.erb +64 -0
  156. data/spec/components/previews/better_ui/table/table_component_preview/sortable.html.erb +44 -0
  157. data/spec/components/previews/better_ui/table/table_component_preview/striped.html.erb +31 -0
  158. data/spec/components/previews/better_ui/table/table_component_preview/with_footer.html.erb +40 -0
  159. data/spec/components/previews/better_ui/table/table_component_preview.rb +85 -0
  160. data/spec/components/previews/better_ui/tabs/container_component_preview/alignments.html.erb +24 -0
  161. data/spec/components/previews/better_ui/tabs/container_component_preview/all_sizes.html.erb +24 -0
  162. data/spec/components/previews/better_ui/tabs/container_component_preview/all_variants.html.erb +24 -0
  163. data/spec/components/previews/better_ui/tabs/container_component_preview/bordered_style.html.erb +30 -0
  164. data/spec/components/previews/better_ui/tabs/container_component_preview/default.html.erb +30 -0
  165. data/spec/components/previews/better_ui/tabs/container_component_preview/disabled_tab.html.erb +65 -0
  166. data/spec/components/previews/better_ui/tabs/container_component_preview/pills_style.html.erb +34 -0
  167. data/spec/components/previews/better_ui/tabs/container_component_preview/turbo_mode.html.erb +40 -0
  168. data/spec/components/previews/better_ui/tabs/container_component_preview/vertical_left.html.erb +38 -0
  169. data/spec/components/previews/better_ui/tabs/container_component_preview/vertical_right.html.erb +30 -0
  170. data/spec/components/previews/better_ui/tabs/container_component_preview/with_icons_and_badges.html.erb +71 -0
  171. data/spec/components/previews/better_ui/tabs/container_component_preview.rb +130 -0
  172. data/spec/components/previews/better_ui/tag_component_preview/all_styles.html.erb +14 -0
  173. data/spec/components/previews/better_ui/tag_component_preview/all_variants.html.erb +14 -0
  174. data/spec/components/previews/better_ui/tag_component_preview/as_links.html.erb +14 -0
  175. data/spec/components/previews/better_ui/tag_component_preview/dismissible.html.erb +34 -0
  176. data/spec/components/previews/better_ui/tag_component_preview.rb +56 -0
  177. data/spec/components/previews/better_ui/tooltip_component_preview/all_positions.html.erb +25 -0
  178. data/spec/components/previews/better_ui/tooltip_component_preview/variants.html.erb +37 -0
  179. data/spec/components/previews/better_ui/tooltip_component_preview.rb +40 -0
  180. metadata +174 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2c218661dc93efb93042ba9a4a5277091a7dac3de216acc162d50c8d36077bc8
4
- data.tar.gz: b06ea500b4d3c547249664dcee953910457263029549d77af2a3314cef45ef58
3
+ metadata.gz: f946c8d53c3641506cd438e13df28f7cc23b3b341294ee790b8fb8ce6a3b5438
4
+ data.tar.gz: 0fa3b052a2588162f89d752460be51e318ed971152e6a903f9cfb76bbd2ca336
5
5
  SHA512:
6
- metadata.gz: 238ed43c059619f7f8ab17ffcc28aa6e6d461b1de72a7bfe57a9a188da44e4f810381ad2c0aac5118b79013855f985635f4bbc01355558640a7f3cb377695fd9
7
- data.tar.gz: 5a8971012b178358d34198642446a423da9ccee69f6418f6d2cc3bc77529bb96a4185e5f281d14b0a6d83fd5195cc20255e61664c7fa4899ef2c06758b63b04e
6
+ metadata.gz: 421babdbde227a74c064a9508b1dc32b4f04a31e0a96d4e3b5677492a5ed1d8e4edab11ad9e8289bf82c04424b73933559fda60a5d030cf5d42fc89fc4eb5f93
7
+ data.tar.gz: 708369a2f8160381dd63e3dd1eedb0524eaf74517369d9e317bb95139f315e6041ae3ce6bfcb536e4ce15b2ab9e31cc6c9cf324b90322b726329284be9332057
data/README.md CHANGED
@@ -304,6 +304,10 @@ BetterUi includes a custom form builder for seamless Rails form integration:
304
304
 
305
305
  > **Note**: You can also use ViewComponent directly with `render BetterUi::*Component.new(...)` if you prefer the explicit rendering syntax.
306
306
 
307
+ ## Live Demo
308
+
309
+ Check out the live example application at [better-ui.pandev.it](https://better-ui.pandev.it) to see all components in action.
310
+
307
311
  ## Documentation
308
312
 
309
313
  - [**Installation Guide**](doc/INSTALLATION.md) - Detailed setup and configuration instructions
@@ -0,0 +1,137 @@
1
+ <nav aria-label="Pagination" class="<%= nav_classes %>">
2
+ <%# Info section %>
3
+ <% if info? %>
4
+ <div class="mb-2 text-sm text-grayscale-600">
5
+ <%= info %>
6
+ </div>
7
+ <% elsif auto_info_text %>
8
+ <div class="mb-2 text-sm text-grayscale-600">
9
+ <%= auto_info_text %>
10
+ </div>
11
+ <% end %>
12
+
13
+ <ul role="list" class="<%= list_classes %>">
14
+ <%# First button %>
15
+ <% if show_first_last %>
16
+ <li>
17
+ <% if first_page? %>
18
+ <span aria-disabled="true" aria-label="First page" class="<%= disabled_classes %>">
19
+ <% if first_label %>
20
+ <%= first_label %>
21
+ <% else %>
22
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="<%= icon_classes %>" aria-hidden="true">
23
+ <path fill-rule="evenodd" d="M15.79 14.77a.75.75 0 01-1.06.02l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 111.04 1.08L11.832 10l3.938 3.71a.75.75 0 01.02 1.06zm-6 0a.75.75 0 01-1.06.02l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 111.04 1.08L5.832 10l3.938 3.71a.75.75 0 01.02 1.06z" clip-rule="evenodd" />
24
+ </svg>
25
+ <% end %>
26
+ </span>
27
+ <% else %>
28
+ <a href="<%= page_url(1) %>" aria-label="First page" class="<%= nav_button_classes %>">
29
+ <% if first_label %>
30
+ <%= first_label %>
31
+ <% else %>
32
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="<%= icon_classes %>" aria-hidden="true">
33
+ <path fill-rule="evenodd" d="M15.79 14.77a.75.75 0 01-1.06.02l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 111.04 1.08L11.832 10l3.938 3.71a.75.75 0 01.02 1.06zm-6 0a.75.75 0 01-1.06.02l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 111.04 1.08L5.832 10l3.938 3.71a.75.75 0 01.02 1.06z" clip-rule="evenodd" />
34
+ </svg>
35
+ <% end %>
36
+ </a>
37
+ <% end %>
38
+ </li>
39
+ <% end %>
40
+
41
+ <%# Previous button %>
42
+ <% if show_prev_next %>
43
+ <li>
44
+ <% if first_page? %>
45
+ <span aria-disabled="true" aria-label="Previous page" class="<%= disabled_classes %>">
46
+ <% if prev_label %>
47
+ <%= prev_label %>
48
+ <% else %>
49
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="<%= icon_classes %>" aria-hidden="true">
50
+ <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
51
+ </svg>
52
+ <% end %>
53
+ </span>
54
+ <% else %>
55
+ <a href="<%= page_url(current_page - 1) %>" rel="prev" aria-label="Previous page" class="<%= nav_button_classes %>">
56
+ <% if prev_label %>
57
+ <%= prev_label %>
58
+ <% else %>
59
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="<%= icon_classes %>" aria-hidden="true">
60
+ <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
61
+ </svg>
62
+ <% end %>
63
+ </a>
64
+ <% end %>
65
+ </li>
66
+ <% end %>
67
+
68
+ <%# Page numbers %>
69
+ <% if show_page_numbers %>
70
+ <% page_items.each do |item| %>
71
+ <li>
72
+ <% if item == :gap %>
73
+ <span aria-hidden="true" class="<%= gap_classes %>"><%= gap_label %></span>
74
+ <% elsif item == current_page %>
75
+ <span aria-current="page" aria-label="Page <%= item %>" class="<%= active_page_classes %>"><%= item %></span>
76
+ <% else %>
77
+ <a href="<%= page_url(item) %>" aria-label="Go to page <%= item %>" class="<%= inactive_page_classes %>"><%= item %></a>
78
+ <% end %>
79
+ </li>
80
+ <% end %>
81
+ <% end %>
82
+
83
+ <%# Next button %>
84
+ <% if show_prev_next %>
85
+ <li>
86
+ <% if last_page? %>
87
+ <span aria-disabled="true" aria-label="Next page" class="<%= disabled_classes %>">
88
+ <% if next_label %>
89
+ <%= next_label %>
90
+ <% else %>
91
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="<%= icon_classes %>" aria-hidden="true">
92
+ <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
93
+ </svg>
94
+ <% end %>
95
+ </span>
96
+ <% else %>
97
+ <a href="<%= page_url(current_page + 1) %>" rel="next" aria-label="Next page" class="<%= nav_button_classes %>">
98
+ <% if next_label %>
99
+ <%= next_label %>
100
+ <% else %>
101
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="<%= icon_classes %>" aria-hidden="true">
102
+ <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
103
+ </svg>
104
+ <% end %>
105
+ </a>
106
+ <% end %>
107
+ </li>
108
+ <% end %>
109
+
110
+ <%# Last button %>
111
+ <% if show_first_last %>
112
+ <li>
113
+ <% if last_page? %>
114
+ <span aria-disabled="true" aria-label="Last page" class="<%= disabled_classes %>">
115
+ <% if last_label %>
116
+ <%= last_label %>
117
+ <% else %>
118
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="<%= icon_classes %>" aria-hidden="true">
119
+ <path fill-rule="evenodd" d="M10.21 14.77a.75.75 0 01-1.06.02l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 111.04 1.08L6.832 10l3.938 3.71a.75.75 0 01.02 1.06zm6 0a.75.75 0 01-1.06.02l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 111.04 1.08L12.832 10l3.938 3.71a.75.75 0 01.02 1.06z" clip-rule="evenodd" />
120
+ </svg>
121
+ <% end %>
122
+ </span>
123
+ <% else %>
124
+ <a href="<%= page_url(total_pages) %>" aria-label="Last page" class="<%= nav_button_classes %>">
125
+ <% if last_label %>
126
+ <%= last_label %>
127
+ <% else %>
128
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="<%= icon_classes %>" aria-hidden="true">
129
+ <path fill-rule="evenodd" d="M10.21 14.77a.75.75 0 01-1.06.02l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 111.04 1.08L6.832 10l3.938 3.71a.75.75 0 01.02 1.06zm6 0a.75.75 0 01-1.06.02l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 111.04 1.08L12.832 10l3.938 3.71a.75.75 0 01.02 1.06z" clip-rule="evenodd" />
130
+ </svg>
131
+ <% end %>
132
+ </a>
133
+ <% end %>
134
+ </li>
135
+ <% end %>
136
+ </ul>
137
+ </nav>
@@ -0,0 +1,408 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module Pagination
5
+ class PaginationComponent < BetterUi::ApplicationComponent
6
+ renders_one :info
7
+
8
+ STYLES = %i[solid outline ghost soft].freeze
9
+
10
+ SIZES = {
11
+ xs: { item: "min-w-6 h-6 text-xs px-1.5", gap: "gap-0.5", icon: "w-3 h-3" },
12
+ sm: { item: "min-w-7 h-7 text-sm px-2", gap: "gap-1", icon: "w-3.5 h-3.5" },
13
+ md: { item: "min-w-9 h-9 text-sm px-3", gap: "gap-1.5", icon: "w-4 h-4" },
14
+ lg: { item: "min-w-11 h-11 text-base px-3.5", gap: "gap-2", icon: "w-5 h-5" },
15
+ xl: { item: "min-w-12 h-12 text-lg px-4", gap: "gap-2.5", icon: "w-5 h-5" }
16
+ }.freeze
17
+
18
+ ROUNDED = {
19
+ none: "rounded-none",
20
+ sm: "rounded-sm",
21
+ md: "rounded-md",
22
+ lg: "rounded-lg",
23
+ full: "rounded-full"
24
+ }.freeze
25
+
26
+ def initialize(
27
+ current_page:,
28
+ total_pages:,
29
+ url:,
30
+ variant: :primary,
31
+ style: :outline,
32
+ size: :md,
33
+ rounded: :md,
34
+ shadow: :none,
35
+ window: 2,
36
+ show_first_last: false,
37
+ show_prev_next: true,
38
+ show_page_numbers: true,
39
+ show_info: false,
40
+ per_page: nil,
41
+ total_count: nil,
42
+ prev_label: nil,
43
+ next_label: nil,
44
+ first_label: nil,
45
+ last_label: nil,
46
+ gap_label: "\u2026",
47
+ container_classes: nil
48
+ )
49
+ @current_page = current_page
50
+ @total_pages = total_pages
51
+ @url = url
52
+ @variant = validate_variant(variant)
53
+ @style = validate_style(style)
54
+ @size = validate_size(size)
55
+ @rounded = validate_rounded(rounded)
56
+ @shadow = normalize_shadow(shadow)
57
+ @window = window
58
+ @show_first_last = show_first_last
59
+ @show_prev_next = show_prev_next
60
+ @show_page_numbers = show_page_numbers
61
+ @show_info = show_info
62
+ @per_page = per_page
63
+ @total_count = total_count
64
+ @prev_label = prev_label
65
+ @next_label = next_label
66
+ @first_label = first_label
67
+ @last_label = last_label
68
+ @gap_label = gap_label
69
+ @container_classes = container_classes
70
+
71
+ validate_pages!
72
+ validate_url!
73
+ end
74
+
75
+ def render?
76
+ @total_pages > 1
77
+ end
78
+
79
+ def page_items
80
+ return [] if @total_pages <= 0
81
+ return [ 1 ] if @total_pages == 1
82
+
83
+ build_page_items
84
+ end
85
+
86
+ def page_url(page)
87
+ @url.call(page)
88
+ end
89
+
90
+ def first_page?
91
+ @current_page == 1
92
+ end
93
+
94
+ def last_page?
95
+ @current_page == @total_pages
96
+ end
97
+
98
+ def auto_info_text
99
+ return nil unless @show_info && @per_page && @total_count
100
+
101
+ from = ((@current_page - 1) * @per_page) + 1
102
+ to = [ @current_page * @per_page, @total_count ].min
103
+ "Showing #{from}-#{to} of #{@total_count} results"
104
+ end
105
+
106
+ private
107
+
108
+ attr_reader :current_page, :total_pages, :variant, :style, :size, :rounded,
109
+ :window, :show_first_last, :show_prev_next, :show_page_numbers,
110
+ :show_info, :per_page, :total_count,
111
+ :prev_label, :next_label, :first_label, :last_label, :gap_label,
112
+ :container_classes
113
+
114
+ # ============================================
115
+ # Page Algorithm
116
+ # ============================================
117
+
118
+ def build_page_items
119
+ window_start = [ @current_page - @window, 1 ].max
120
+ window_end = [ @current_page + @window, @total_pages ].min
121
+
122
+ # If everything fits without gaps, return full range
123
+ return (1..@total_pages).to_a if @total_pages <= (2 * @window + 3)
124
+
125
+ items = []
126
+
127
+ # Always include first page
128
+ items << 1
129
+
130
+ # Gap or bridging pages between first and window
131
+ if window_start > 2
132
+ if window_start == 3
133
+ items << 2
134
+ else
135
+ items << :gap
136
+ end
137
+ end
138
+
139
+ # Window pages (skip first and last as they are always included)
140
+ (window_start..window_end).each do |page|
141
+ items << page unless items.include?(page)
142
+ end
143
+
144
+ # Gap or bridging pages between window and last
145
+ if window_end < @total_pages - 1
146
+ if window_end == @total_pages - 2
147
+ items << @total_pages - 1
148
+ else
149
+ items << :gap
150
+ end
151
+ end
152
+
153
+ # Always include last page
154
+ items << @total_pages unless items.include?(@total_pages)
155
+
156
+ items
157
+ end
158
+
159
+ # ============================================
160
+ # CSS Classes
161
+ # ============================================
162
+
163
+ def nav_classes
164
+ classes = [ "flex flex-col items-center" ]
165
+ classes << SHADOWS[@shadow] if SHADOWS[@shadow]
166
+ classes << @container_classes if @container_classes
167
+ css_classes(*classes)
168
+ end
169
+
170
+ def list_classes
171
+ css_classes("flex items-center", SIZES[@size][:gap])
172
+ end
173
+
174
+ def item_base_classes
175
+ css_classes(
176
+ "inline-flex items-center justify-center font-medium transition-colors duration-200",
177
+ SIZES[@size][:item],
178
+ ROUNDED[@rounded]
179
+ )
180
+ end
181
+
182
+ def active_page_classes
183
+ css_classes(item_base_classes, active_style_classes)
184
+ end
185
+
186
+ def inactive_page_classes
187
+ css_classes(item_base_classes, inactive_style_classes)
188
+ end
189
+
190
+ def disabled_classes
191
+ css_classes(item_base_classes, "opacity-40 cursor-not-allowed pointer-events-none", disabled_color_classes)
192
+ end
193
+
194
+ def gap_classes
195
+ css_classes(
196
+ "inline-flex items-center justify-center",
197
+ SIZES[@size][:item],
198
+ "text-grayscale-400 select-none"
199
+ )
200
+ end
201
+
202
+ def nav_button_classes
203
+ css_classes(item_base_classes, inactive_style_classes)
204
+ end
205
+
206
+ def icon_classes
207
+ SIZES[@size][:icon]
208
+ end
209
+
210
+ # ============================================
211
+ # Style-specific active/inactive classes
212
+ # ============================================
213
+
214
+ def active_style_classes
215
+ case @style
216
+ when :solid then solid_active_classes
217
+ when :outline then outline_active_classes
218
+ when :ghost then ghost_active_classes
219
+ when :soft then soft_active_classes
220
+ end
221
+ end
222
+
223
+ def inactive_style_classes
224
+ case @style
225
+ when :solid then solid_inactive_classes
226
+ when :outline then outline_inactive_classes
227
+ when :ghost then ghost_inactive_classes
228
+ when :soft then soft_inactive_classes
229
+ end
230
+ end
231
+
232
+ def disabled_color_classes
233
+ case @style
234
+ when :solid, :soft
235
+ "bg-grayscale-100 text-grayscale-400"
236
+ when :outline
237
+ "border border-grayscale-200 text-grayscale-400"
238
+ when :ghost
239
+ "text-grayscale-400"
240
+ end
241
+ end
242
+
243
+ # --- Solid ---
244
+
245
+ def solid_active_classes
246
+ case @variant
247
+ when :primary then "bg-primary-600 text-white"
248
+ when :secondary then "bg-secondary-600 text-white"
249
+ when :accent then "bg-accent-600 text-white"
250
+ when :success then "bg-success-600 text-white"
251
+ when :danger then "bg-danger-600 text-white"
252
+ when :warning then "bg-warning-600 text-white"
253
+ when :info then "bg-info-600 text-white"
254
+ when :light then "bg-grayscale-200 text-grayscale-900"
255
+ when :dark then "bg-grayscale-900 text-grayscale-50"
256
+ end
257
+ end
258
+
259
+ def solid_inactive_classes
260
+ case @variant
261
+ when :primary then "text-grayscale-700 hover:bg-primary-50 hover:text-primary-700"
262
+ when :secondary then "text-grayscale-700 hover:bg-secondary-50 hover:text-secondary-700"
263
+ when :accent then "text-grayscale-700 hover:bg-accent-50 hover:text-accent-700"
264
+ when :success then "text-grayscale-700 hover:bg-success-50 hover:text-success-700"
265
+ when :danger then "text-grayscale-700 hover:bg-danger-50 hover:text-danger-700"
266
+ when :warning then "text-grayscale-700 hover:bg-warning-50 hover:text-warning-700"
267
+ when :info then "text-grayscale-700 hover:bg-info-50 hover:text-info-700"
268
+ when :light then "text-grayscale-700 hover:bg-grayscale-100 hover:text-grayscale-900"
269
+ when :dark then "text-grayscale-700 hover:bg-grayscale-800 hover:text-grayscale-50"
270
+ end
271
+ end
272
+
273
+ # --- Outline ---
274
+
275
+ def outline_active_classes
276
+ case @variant
277
+ when :primary then "border border-primary-600 bg-primary-50 text-primary-700"
278
+ when :secondary then "border border-secondary-600 bg-secondary-50 text-secondary-700"
279
+ when :accent then "border border-accent-600 bg-accent-50 text-accent-700"
280
+ when :success then "border border-success-600 bg-success-50 text-success-700"
281
+ when :danger then "border border-danger-600 bg-danger-50 text-danger-700"
282
+ when :warning then "border border-warning-600 bg-warning-50 text-warning-700"
283
+ when :info then "border border-info-600 bg-info-50 text-info-700"
284
+ when :light then "border border-grayscale-400 bg-grayscale-50 text-grayscale-900"
285
+ when :dark then "border border-grayscale-700 bg-grayscale-800 text-grayscale-50"
286
+ end
287
+ end
288
+
289
+ def outline_inactive_classes
290
+ case @variant
291
+ when :primary then "border border-grayscale-200 text-grayscale-700 hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700"
292
+ when :secondary then "border border-grayscale-200 text-grayscale-700 hover:border-secondary-300 hover:bg-secondary-50 hover:text-secondary-700"
293
+ when :accent then "border border-grayscale-200 text-grayscale-700 hover:border-accent-300 hover:bg-accent-50 hover:text-accent-700"
294
+ when :success then "border border-grayscale-200 text-grayscale-700 hover:border-success-300 hover:bg-success-50 hover:text-success-700"
295
+ when :danger then "border border-grayscale-200 text-grayscale-700 hover:border-danger-300 hover:bg-danger-50 hover:text-danger-700"
296
+ when :warning then "border border-grayscale-200 text-grayscale-700 hover:border-warning-300 hover:bg-warning-50 hover:text-warning-700"
297
+ when :info then "border border-grayscale-200 text-grayscale-700 hover:border-info-300 hover:bg-info-50 hover:text-info-700"
298
+ when :light then "border border-grayscale-200 text-grayscale-700 hover:border-grayscale-400 hover:bg-grayscale-50 hover:text-grayscale-900"
299
+ when :dark then "border border-grayscale-200 text-grayscale-700 hover:border-grayscale-600 hover:bg-grayscale-800 hover:text-grayscale-50"
300
+ end
301
+ end
302
+
303
+ # --- Ghost ---
304
+
305
+ def ghost_active_classes
306
+ case @variant
307
+ when :primary then "bg-primary-100 text-primary-700"
308
+ when :secondary then "bg-secondary-100 text-secondary-700"
309
+ when :accent then "bg-accent-100 text-accent-700"
310
+ when :success then "bg-success-100 text-success-700"
311
+ when :danger then "bg-danger-100 text-danger-700"
312
+ when :warning then "bg-warning-100 text-warning-700"
313
+ when :info then "bg-info-100 text-info-700"
314
+ when :light then "bg-grayscale-200 text-grayscale-900"
315
+ when :dark then "bg-grayscale-800 text-grayscale-50"
316
+ end
317
+ end
318
+
319
+ def ghost_inactive_classes
320
+ case @variant
321
+ when :primary then "text-grayscale-700 hover:bg-primary-50 hover:text-primary-700"
322
+ when :secondary then "text-grayscale-700 hover:bg-secondary-50 hover:text-secondary-700"
323
+ when :accent then "text-grayscale-700 hover:bg-accent-50 hover:text-accent-700"
324
+ when :success then "text-grayscale-700 hover:bg-success-50 hover:text-success-700"
325
+ when :danger then "text-grayscale-700 hover:bg-danger-50 hover:text-danger-700"
326
+ when :warning then "text-grayscale-700 hover:bg-warning-50 hover:text-warning-700"
327
+ when :info then "text-grayscale-700 hover:bg-info-50 hover:text-info-700"
328
+ when :light then "text-grayscale-700 hover:bg-grayscale-100 hover:text-grayscale-900"
329
+ when :dark then "text-grayscale-700 hover:bg-grayscale-800 hover:text-grayscale-50"
330
+ end
331
+ end
332
+
333
+ # --- Soft ---
334
+
335
+ def soft_active_classes
336
+ case @variant
337
+ when :primary then "bg-primary-100 text-primary-700 font-semibold"
338
+ when :secondary then "bg-secondary-100 text-secondary-700 font-semibold"
339
+ when :accent then "bg-accent-100 text-accent-700 font-semibold"
340
+ when :success then "bg-success-100 text-success-700 font-semibold"
341
+ when :danger then "bg-danger-100 text-danger-700 font-semibold"
342
+ when :warning then "bg-warning-100 text-warning-700 font-semibold"
343
+ when :info then "bg-info-100 text-info-700 font-semibold"
344
+ when :light then "bg-grayscale-200 text-grayscale-900 font-semibold"
345
+ when :dark then "bg-grayscale-800 text-grayscale-50 font-semibold"
346
+ end
347
+ end
348
+
349
+ def soft_inactive_classes
350
+ case @variant
351
+ when :primary then "bg-primary-50 text-grayscale-700 hover:bg-primary-100 hover:text-primary-700"
352
+ when :secondary then "bg-secondary-50 text-grayscale-700 hover:bg-secondary-100 hover:text-secondary-700"
353
+ when :accent then "bg-accent-50 text-grayscale-700 hover:bg-accent-100 hover:text-accent-700"
354
+ when :success then "bg-success-50 text-grayscale-700 hover:bg-success-100 hover:text-success-700"
355
+ when :danger then "bg-danger-50 text-grayscale-700 hover:bg-danger-100 hover:text-danger-700"
356
+ when :warning then "bg-warning-50 text-grayscale-700 hover:bg-warning-100 hover:text-warning-700"
357
+ when :info then "bg-info-50 text-grayscale-700 hover:bg-info-100 hover:text-info-700"
358
+ when :light then "bg-grayscale-100 text-grayscale-700 hover:bg-grayscale-200 hover:text-grayscale-900"
359
+ when :dark then "bg-grayscale-700 text-grayscale-200 hover:bg-grayscale-800 hover:text-grayscale-50"
360
+ end
361
+ end
362
+
363
+ # ============================================
364
+ # Validation
365
+ # ============================================
366
+
367
+ def validate_pages!
368
+ raise ArgumentError, "total_pages must be >= 0" if @total_pages.negative?
369
+ raise ArgumentError, "current_page must be >= 1" if @total_pages > 0 && @current_page < 1
370
+ if @total_pages > 0 && @current_page > @total_pages
371
+ raise ArgumentError, "current_page (#{@current_page}) cannot exceed total_pages (#{@total_pages})"
372
+ end
373
+ end
374
+
375
+ def validate_url!
376
+ raise ArgumentError, "url must be a Proc" unless @url.is_a?(Proc)
377
+ end
378
+
379
+ def validate_variant(variant)
380
+ unless BetterUi::ApplicationComponent::VARIANTS.key?(variant)
381
+ raise ArgumentError, "Invalid variant: #{variant}. Must be one of: #{BetterUi::ApplicationComponent::VARIANTS.keys.join(', ')}"
382
+ end
383
+ variant
384
+ end
385
+
386
+ def validate_style(style)
387
+ unless STYLES.include?(style)
388
+ raise ArgumentError, "Invalid style: #{style}. Must be one of: #{STYLES.join(', ')}"
389
+ end
390
+ style
391
+ end
392
+
393
+ def validate_size(size)
394
+ unless SIZES.key?(size)
395
+ raise ArgumentError, "Invalid size: #{size}. Must be one of: #{SIZES.keys.join(', ')}"
396
+ end
397
+ size
398
+ end
399
+
400
+ def validate_rounded(rounded)
401
+ unless ROUNDED.key?(rounded)
402
+ raise ArgumentError, "Invalid rounded: #{rounded}. Must be one of: #{ROUNDED.keys.join(', ')}"
403
+ end
404
+ rounded
405
+ end
406
+ end
407
+ end
408
+ end
@@ -37,11 +37,11 @@
37
37
  <% if body_row_partial? %>
38
38
  <%= render partial: @body_row_partial, locals: { item: item, index: index, columns: columns } %>
39
39
  <% else %>
40
- <tr class="<%= collection_row_classes(item) %>">
40
+ <%= tag.tr(**collection_row_attributes(item, index)) do %>
41
41
  <% columns.each do |column| %>
42
42
  <td class="<%= collection_cell_classes(column) %>"><%= column.value_for(item) %></td>
43
43
  <% end %>
44
- </tr>
44
+ <% end %>
45
45
  <% end %>
46
46
  <% end %>
47
47
  </tbody>
@@ -89,6 +89,7 @@ module BetterUi
89
89
  caption: nil,
90
90
  collection: nil,
91
91
  row_highlighted: nil,
92
+ row_html: nil,
92
93
  body_row_partial: nil,
93
94
  header_partial: nil,
94
95
  footer_partial: nil,
@@ -110,6 +111,7 @@ module BetterUi
110
111
  @caption = caption
111
112
  @collection = collection
112
113
  @row_highlighted = row_highlighted
114
+ @row_html = row_html
113
115
  @body_row_partial = body_row_partial
114
116
  @header_partial = header_partial
115
117
  @footer_partial = footer_partial
@@ -288,6 +290,34 @@ module BetterUi
288
290
  ].compact)
289
291
  end
290
292
 
293
+ # Collection mode: full row attributes (classes + custom HTML attrs from row_html proc)
294
+ def collection_row_attributes(item, index)
295
+ base_classes = collection_row_classes(item)
296
+ custom_attrs = resolve_row_html(item, index)
297
+ custom_class = custom_attrs.delete(:class)
298
+ merged_class = custom_class ? css_classes(base_classes, custom_class) : base_classes
299
+ { class: merged_class, **custom_attrs }
300
+ end
301
+
302
+ # Resolve row_html proc to a hash of HTML attributes
303
+ def resolve_row_html(item, index)
304
+ return {} if @row_html.nil?
305
+
306
+ result = if @row_html.arity == 1
307
+ @row_html.call(item)
308
+ else
309
+ @row_html.call(item, index)
310
+ end
311
+
312
+ return {} if result.nil?
313
+
314
+ unless result.is_a?(Hash)
315
+ raise ArgumentError, "row_html proc must return a Hash or nil, got #{result.class}"
316
+ end
317
+
318
+ result.symbolize_keys
319
+ end
320
+
291
321
  def collection_striped_classes
292
322
  return nil unless @striped
293
323
  case @variant