better_ui 0.9.1 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bfebd7a7f2bdd44a6d4e0bde15d4608c970ddab81a6d4cdd2454fead24711239
4
- data.tar.gz: '09f376342d514046ba25b525f35d499be58493aadccca8b40ad71bbf2c3b2912'
3
+ metadata.gz: f946c8d53c3641506cd438e13df28f7cc23b3b341294ee790b8fb8ce6a3b5438
4
+ data.tar.gz: 0fa3b052a2588162f89d752460be51e318ed971152e6a903f9cfb76bbd2ca336
5
5
  SHA512:
6
- metadata.gz: 48fcb5c0cfcea7748056c19b5f36791498e98dce7bd2a4af0d0e43d7f7f5374495b42bc73b4fd2ee1dbec6fa9dd219185206422580cbf07b32514f1fd58def54
7
- data.tar.gz: 155edcbbfbbea18cfb4b4b7c4b26fb5b9fd4ae040818f94e00f82410e10d129a968b4d5b575c392a8b0ac708f45bbc1e441d29159d89fd1952cb9a5836cc9467
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
@@ -744,6 +744,8 @@ module BetterUi
744
744
  # @option options [Boolean] :responsive Horizontal scroll wrapper (default: true)
745
745
  # @option options [String, nil] :caption Table caption text
746
746
  # @option options [Array, nil] :collection Data collection (triggers collection mode)
747
+ # @option options [Proc, nil] :row_html Proc returning a Hash of HTML attributes for each <tr> in collection mode.
748
+ # Accepts 1-arg `(item)` or 2-arg `(item, index)`. Return nil for no-op. Classes are merged with built-in classes.
747
749
  # @yield [table] Block with table slots
748
750
  # @yieldparam table [BetterUi::Table::TableComponent] The table component
749
751
  # @return [String] Rendered HTML
@@ -900,5 +902,77 @@ module BetterUi
900
902
  def bui_dropdown(**options, &block)
901
903
  render BetterUi::Dropdown::DropdownComponent.new(**options), &block
902
904
  end
905
+
906
+ # ============================================
907
+ # Pagination Components
908
+ # ============================================
909
+
910
+ # Renders a pagination component for navigating between pages.
911
+ #
912
+ # @param options [Hash] Options passed to Pagination::PaginationComponent
913
+ # @option options [Integer] :current_page Current page number (1-indexed, required)
914
+ # @option options [Integer] :total_pages Total number of pages (required)
915
+ # @option options [Proc] :url Proc that receives a page number and returns a URL (required)
916
+ # @option options [Symbol] :variant Color variant (:primary, :secondary, :accent, :success, :danger, :warning, :info, :light, :dark)
917
+ # @option options [Symbol] :style Pagination style (:solid, :outline, :ghost, :soft)
918
+ # @option options [Symbol] :size Size (:xs, :sm, :md, :lg, :xl)
919
+ # @option options [Symbol] :rounded Border radius (:none, :sm, :md, :lg, :full)
920
+ # @option options [Symbol] :shadow Shadow size (:none, :sm, :md, :lg, :xl)
921
+ # @option options [Integer] :window Pages shown each side of current (default: 2)
922
+ # @option options [Boolean] :show_first_last Show first/last buttons (default: false)
923
+ # @option options [Boolean] :show_prev_next Show prev/next buttons (default: true)
924
+ # @option options [Boolean] :show_page_numbers Show numbered pages (default: true)
925
+ # @option options [Boolean] :show_info Auto-generate info text (default: false)
926
+ # @option options [Integer, nil] :per_page Items per page (for auto info text)
927
+ # @option options [Integer, nil] :total_count Total item count (for auto info text)
928
+ # @option options [String, nil] :prev_label Custom previous button text (nil = SVG icon)
929
+ # @option options [String, nil] :next_label Custom next button text (nil = SVG icon)
930
+ # @option options [String, nil] :first_label Custom first button text (nil = SVG icon)
931
+ # @option options [String, nil] :last_label Custom last button text (nil = SVG icon)
932
+ # @option options [String] :gap_label Ellipsis character (default: "...")
933
+ # @option options [String, nil] :container_classes Additional CSS classes on nav
934
+ # @yield [pagination] Block with pagination slots
935
+ # @yieldparam pagination [BetterUi::Pagination::PaginationComponent] The pagination component for slot access
936
+ # @return [String] Rendered HTML
937
+ #
938
+ # @example Basic pagination
939
+ # <%= bui_pagination(current_page: 5, total_pages: 20, url: ->(p) { users_path(page: p) }) %>
940
+ #
941
+ # @example With info slot
942
+ # <%= bui_pagination(current_page: 5, total_pages: 20, url: ->(p) { users_path(page: p) }) do |pg| %>
943
+ # <% pg.with_info { "Showing 41-50 of 200 results" } %>
944
+ # <% end %>
945
+ def bui_pagination(**options, &block)
946
+ render BetterUi::Pagination::PaginationComponent.new(**options), &block
947
+ end
948
+
949
+ # Renders a pagination component from a Pagy object.
950
+ #
951
+ # Convenience helper for Pagy users. Extracts current_page, total_pages,
952
+ # total_count, and per_page from the Pagy object. Does not add a Pagy
953
+ # gem dependency -- uses the host app's `pagy_url_for` helper.
954
+ #
955
+ # @param pagy [Object] Pagy pagination object
956
+ # @param options [Hash] Options passed to Pagination::PaginationComponent (see {#bui_pagination})
957
+ # @option options [Proc, nil] :url Custom URL proc (default: uses pagy_url_for)
958
+ # @yield [pagination] Block with pagination slots
959
+ # @return [String] Rendered HTML
960
+ #
961
+ # @example Basic Pagy usage
962
+ # <%= bui_pagination_for(@pagy) %>
963
+ #
964
+ # @example With options
965
+ # <%= bui_pagination_for(@pagy, variant: :success, show_first_last: true) %>
966
+ def bui_pagination_for(pagy, **options, &block)
967
+ url_proc = options.delete(:url) || ->(page) { pagy_url_for(pagy, page) }
968
+ render BetterUi::Pagination::PaginationComponent.new(
969
+ current_page: pagy.page,
970
+ total_pages: pagy.last,
971
+ url: url_proc,
972
+ total_count: pagy.count,
973
+ per_page: pagy.vars[:items],
974
+ **options
975
+ ), &block
976
+ end
903
977
  end
904
978
  end
@@ -1,3 +1,3 @@
1
1
  module BetterUi
2
- VERSION = "0.9.1"
2
+ VERSION = "0.10.0"
3
3
  end
@@ -0,0 +1,13 @@
1
+ <div class="p-8 space-y-8">
2
+ <% (@sizes || [:xs, :sm, :md, :lg, :xl]).each do |size| %>
3
+ <div>
4
+ <h3 class="text-lg font-semibold mb-3 capitalize"><%= size %></h3>
5
+ <%= render BetterUi::Pagination::PaginationComponent.new(
6
+ current_page: 5,
7
+ total_pages: 20,
8
+ url: ->(page) { "#page-#{page}" },
9
+ size: size
10
+ ) %>
11
+ </div>
12
+ <% end %>
13
+ </div>
@@ -0,0 +1,13 @@
1
+ <div class="p-8 space-y-8">
2
+ <% (@styles || [:solid, :outline, :ghost, :soft]).each do |style| %>
3
+ <div>
4
+ <h3 class="text-lg font-semibold mb-3 capitalize"><%= style %></h3>
5
+ <%= render BetterUi::Pagination::PaginationComponent.new(
6
+ current_page: 5,
7
+ total_pages: 20,
8
+ url: ->(page) { "#page-#{page}" },
9
+ style: style
10
+ ) %>
11
+ </div>
12
+ <% end %>
13
+ </div>
@@ -0,0 +1,13 @@
1
+ <div class="p-8 space-y-8">
2
+ <% (@variants || BetterUi::ApplicationComponent::VARIANTS.keys).each do |variant| %>
3
+ <div>
4
+ <h3 class="text-lg font-semibold mb-3 capitalize"><%= variant %></h3>
5
+ <%= render BetterUi::Pagination::PaginationComponent.new(
6
+ current_page: 5,
7
+ total_pages: 20,
8
+ url: ->(page) { "#page-#{page}" },
9
+ variant: variant
10
+ ) %>
11
+ </div>
12
+ <% end %>
13
+ </div>
@@ -0,0 +1,28 @@
1
+ <div class="p-8 space-y-6">
2
+ <h3 class="text-lg font-semibold mb-4">Default Pagination</h3>
3
+ <%= render BetterUi::Pagination::PaginationComponent.new(
4
+ current_page: 5,
5
+ total_pages: 20,
6
+ url: ->(page) { "#page-#{page}" }
7
+ ) %>
8
+
9
+ <h3 class="text-lg font-semibold mb-4 mt-8">With First/Last Buttons</h3>
10
+ <%= render BetterUi::Pagination::PaginationComponent.new(
11
+ current_page: 10,
12
+ total_pages: 20,
13
+ url: ->(page) { "#page-#{page}" },
14
+ show_first_last: true
15
+ ) %>
16
+
17
+ <h3 class="text-lg font-semibold mb-4 mt-8">With Custom Labels</h3>
18
+ <%= render BetterUi::Pagination::PaginationComponent.new(
19
+ current_page: 5,
20
+ total_pages: 20,
21
+ url: ->(page) { "#page-#{page}" },
22
+ show_first_last: true,
23
+ first_label: "First",
24
+ last_label: "Last",
25
+ prev_label: "Prev",
26
+ next_label: "Next"
27
+ ) %>
28
+ </div>
@@ -0,0 +1,82 @@
1
+ <div class="p-8 space-y-8">
2
+ <div>
3
+ <h3 class="text-lg font-semibold mb-3">2 Pages (first page)</h3>
4
+ <%= render BetterUi::Pagination::PaginationComponent.new(
5
+ current_page: 1,
6
+ total_pages: 2,
7
+ url: ->(page) { "#page-#{page}" }
8
+ ) %>
9
+ </div>
10
+
11
+ <div>
12
+ <h3 class="text-lg font-semibold mb-3">2 Pages (last page)</h3>
13
+ <%= render BetterUi::Pagination::PaginationComponent.new(
14
+ current_page: 2,
15
+ total_pages: 2,
16
+ url: ->(page) { "#page-#{page}" }
17
+ ) %>
18
+ </div>
19
+
20
+ <div>
21
+ <h3 class="text-lg font-semibold mb-3">5 Pages (no gaps)</h3>
22
+ <%= render BetterUi::Pagination::PaginationComponent.new(
23
+ current_page: 3,
24
+ total_pages: 5,
25
+ url: ->(page) { "#page-#{page}" }
26
+ ) %>
27
+ </div>
28
+
29
+ <div>
30
+ <h3 class="text-lg font-semibold mb-3">First Page of Many</h3>
31
+ <%= render BetterUi::Pagination::PaginationComponent.new(
32
+ current_page: 1,
33
+ total_pages: 100,
34
+ url: ->(page) { "#page-#{page}" },
35
+ show_first_last: true
36
+ ) %>
37
+ </div>
38
+
39
+ <div>
40
+ <h3 class="text-lg font-semibold mb-3">Last Page of Many</h3>
41
+ <%= render BetterUi::Pagination::PaginationComponent.new(
42
+ current_page: 100,
43
+ total_pages: 100,
44
+ url: ->(page) { "#page-#{page}" },
45
+ show_first_last: true
46
+ ) %>
47
+ </div>
48
+
49
+ <div>
50
+ <h3 class="text-lg font-semibold mb-3">Middle of Many (dual gap)</h3>
51
+ <%= render BetterUi::Pagination::PaginationComponent.new(
52
+ current_page: 50,
53
+ total_pages: 100,
54
+ url: ->(page) { "#page-#{page}" },
55
+ show_first_last: true
56
+ ) %>
57
+ </div>
58
+
59
+ <div>
60
+ <h3 class="text-lg font-semibold mb-3">Prev/Next Only (no page numbers)</h3>
61
+ <%= render BetterUi::Pagination::PaginationComponent.new(
62
+ current_page: 5,
63
+ total_pages: 20,
64
+ url: ->(page) { "#page-#{page}" },
65
+ show_page_numbers: false,
66
+ prev_label: "Previous",
67
+ next_label: "Next"
68
+ ) %>
69
+ </div>
70
+
71
+ <div>
72
+ <h3 class="text-lg font-semibold mb-3">Full Rounded</h3>
73
+ <%= render BetterUi::Pagination::PaginationComponent.new(
74
+ current_page: 5,
75
+ total_pages: 20,
76
+ url: ->(page) { "#page-#{page}" },
77
+ rounded: :full,
78
+ style: :solid,
79
+ variant: :success
80
+ ) %>
81
+ </div>
82
+ </div>
@@ -0,0 +1,38 @@
1
+ <div class="p-8 space-y-8">
2
+ <div>
3
+ <h3 class="text-lg font-semibold mb-3">Auto Info Text</h3>
4
+ <%= render BetterUi::Pagination::PaginationComponent.new(
5
+ current_page: 3,
6
+ total_pages: 10,
7
+ url: ->(page) { "#page-#{page}" },
8
+ show_info: true,
9
+ per_page: 20,
10
+ total_count: 195
11
+ ) %>
12
+ </div>
13
+
14
+ <div>
15
+ <h3 class="text-lg font-semibold mb-3">Custom Info Slot</h3>
16
+ <%= render BetterUi::Pagination::PaginationComponent.new(
17
+ current_page: 3,
18
+ total_pages: 10,
19
+ url: ->(page) { "#page-#{page}" }
20
+ ) do |pg| %>
21
+ <% pg.with_info do %>
22
+ <strong>Page 3</strong> of 10 &mdash; Displaying items 41&ndash;60
23
+ <% end %>
24
+ <% end %>
25
+ </div>
26
+
27
+ <div>
28
+ <h3 class="text-lg font-semibold mb-3">Last Page Info (partial page)</h3>
29
+ <%= render BetterUi::Pagination::PaginationComponent.new(
30
+ current_page: 10,
31
+ total_pages: 10,
32
+ url: ->(page) { "#page-#{page}" },
33
+ show_info: true,
34
+ per_page: 20,
35
+ total_count: 195
36
+ ) %>
37
+ </div>
38
+ </div>
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module Pagination
5
+ class PaginationComponentPreview < ViewComponent::Preview
6
+ # @label Default
7
+ def default
8
+ render_with_template
9
+ end
10
+
11
+ # @label All Variants
12
+ # @display bg_color #f5f5f5
13
+ def all_variants
14
+ @variants = BetterUi::ApplicationComponent::VARIANTS.keys
15
+ render_with_template
16
+ end
17
+
18
+ # @label All Styles
19
+ # @display bg_color #f5f5f5
20
+ def all_styles
21
+ @styles = [ :solid, :outline, :ghost, :soft ]
22
+ render_with_template
23
+ end
24
+
25
+ # @label All Sizes
26
+ # @display bg_color #f5f5f5
27
+ def all_sizes
28
+ @sizes = [ :xs, :sm, :md, :lg, :xl ]
29
+ render_with_template
30
+ end
31
+
32
+ # @label With Info
33
+ def with_info
34
+ render_with_template
35
+ end
36
+
37
+ # @label Edge Cases
38
+ def edge_cases
39
+ render_with_template
40
+ end
41
+
42
+ # @label Playground
43
+ # @param current_page number
44
+ # @param total_pages number
45
+ # @param variant select { choices: [primary, secondary, accent, success, danger, warning, info, light, dark] }
46
+ # @param style select { choices: [solid, outline, ghost, soft] }
47
+ # @param size select { choices: [xs, sm, md, lg, xl] }
48
+ # @param rounded select { choices: [none, sm, md, lg, full] }
49
+ # @param window number
50
+ # @param show_first_last toggle
51
+ # @param show_prev_next toggle
52
+ # @param show_page_numbers toggle
53
+ def playground(
54
+ current_page: 5,
55
+ total_pages: 20,
56
+ variant: :primary,
57
+ style: :outline,
58
+ size: :md,
59
+ rounded: :md,
60
+ window: 2,
61
+ show_first_last: false,
62
+ show_prev_next: true,
63
+ show_page_numbers: true
64
+ )
65
+ current_page = current_page.to_i
66
+ total_pages = total_pages.to_i
67
+ window = window.to_i
68
+ show_first_last = ActiveModel::Type::Boolean.new.cast(show_first_last)
69
+ show_prev_next = ActiveModel::Type::Boolean.new.cast(show_prev_next)
70
+ show_page_numbers = ActiveModel::Type::Boolean.new.cast(show_page_numbers)
71
+
72
+ render BetterUi::Pagination::PaginationComponent.new(
73
+ current_page: current_page,
74
+ total_pages: total_pages,
75
+ url: ->(page) { "#page-#{page}" },
76
+ variant: variant,
77
+ style: style,
78
+ size: size,
79
+ rounded: rounded,
80
+ window: window,
81
+ show_first_last: show_first_last,
82
+ show_prev_next: show_prev_next,
83
+ show_page_numbers: show_page_numbers
84
+ )
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,64 @@
1
+ <div class="max-w-4xl mx-auto p-6">
2
+ <h2 class="text-xl font-bold mb-4">Row HTML: Data Attributes</h2>
3
+ <p class="text-sm text-grayscale-500 mb-4">Each row gets custom data attributes from the item.</p>
4
+
5
+ <%
6
+ users = [
7
+ OpenStruct.new(id: 1, name: "Alice Johnson", email: "alice@example.com", role: "Admin"),
8
+ OpenStruct.new(id: 2, name: "Bob Smith", email: "bob@example.com", role: "Editor"),
9
+ OpenStruct.new(id: 3, name: "Charlie Brown", email: "charlie@example.com", role: "Viewer")
10
+ ]
11
+ %>
12
+
13
+ <%= render BetterUi::Table::TableComponent.new(
14
+ collection: users,
15
+ variant: :primary,
16
+ row_html: ->(user) { { data: { id: user.id, role: user.role.downcase } } }
17
+ ) do |t| %>
18
+ <% t.with_column(key: :name, label: "Name") %>
19
+ <% t.with_column(key: :email, label: "Email") %>
20
+ <% t.with_column(key: :role, label: "Role") %>
21
+ <% end %>
22
+
23
+ <h2 class="text-xl font-bold mb-4 mt-8">Row HTML: Custom Classes + Striped</h2>
24
+ <p class="text-sm text-grayscale-500 mb-4">Admin rows get bold text, merged with striped styling.</p>
25
+
26
+ <%= render BetterUi::Table::TableComponent.new(
27
+ collection: users,
28
+ variant: :info,
29
+ striped: true,
30
+ row_html: ->(user) { { class: ("font-bold text-info-800" if user.role == "Admin") } }
31
+ ) do |t| %>
32
+ <% t.with_column(key: :name, label: "Name") %>
33
+ <% t.with_column(key: :email, label: "Email") %>
34
+ <% t.with_column(key: :role, label: "Role") %>
35
+ <% end %>
36
+
37
+ <h2 class="text-xl font-bold mb-4 mt-8">Row HTML: Index-based IDs</h2>
38
+ <p class="text-sm text-grayscale-500 mb-4">2-argument proc receives item and index for row IDs.</p>
39
+
40
+ <%= render BetterUi::Table::TableComponent.new(
41
+ collection: users,
42
+ variant: :secondary,
43
+ row_html: ->(_user, idx) { { id: "user-row-#{idx}" } }
44
+ ) do |t| %>
45
+ <% t.with_column(key: :name, label: "Name") %>
46
+ <% t.with_column(key: :email, label: "Email") %>
47
+ <% t.with_column(key: :role, label: "Role") %>
48
+ <% end %>
49
+
50
+ <h2 class="text-xl font-bold mb-4 mt-8">Row HTML: Combined with Highlighting</h2>
51
+ <p class="text-sm text-grayscale-500 mb-4">row_html and row_highlighted work together.</p>
52
+
53
+ <%= render BetterUi::Table::TableComponent.new(
54
+ collection: users,
55
+ variant: :success,
56
+ hoverable: true,
57
+ row_highlighted: ->(user) { user.role == "Admin" },
58
+ row_html: ->(user) { { data: { id: user.id }, class: ("border-l-4 border-success-500" if user.role == "Admin") } }
59
+ ) do |t| %>
60
+ <% t.with_column(key: :name, label: "Name") %>
61
+ <% t.with_column(key: :email, label: "Email") %>
62
+ <% t.with_column(key: :role, label: "Role") %>
63
+ <% end %>
64
+ </div>
@@ -74,6 +74,12 @@ module BetterUi
74
74
  def sortable
75
75
  render_with_template
76
76
  end
77
+
78
+ # @label Row HTML Customization
79
+ # @display bg_color "#f5f5f5"
80
+ def row_html
81
+ render_with_template
82
+ end
77
83
  end
78
84
  end
79
85
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: better_ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Umberto Peserico
@@ -149,6 +149,8 @@ files:
149
149
  - app/components/better_ui/heading_component/heading_component.html.erb
150
150
  - app/components/better_ui/link_component.rb
151
151
  - app/components/better_ui/link_component/link_component.html.erb
152
+ - app/components/better_ui/pagination/pagination_component.rb
153
+ - app/components/better_ui/pagination/pagination_component/pagination_component.html.erb
152
154
  - app/components/better_ui/progress_component.rb
153
155
  - app/components/better_ui/progress_component/progress_component.html.erb
154
156
  - app/components/better_ui/spinner_component.rb
@@ -311,6 +313,13 @@ files:
311
313
  - spec/components/previews/better_ui/link_component_preview/all_styles.html.erb
312
314
  - spec/components/previews/better_ui/link_component_preview/all_variants.html.erb
313
315
  - spec/components/previews/better_ui/link_component_preview/with_icons.html.erb
316
+ - spec/components/previews/better_ui/pagination/pagination_component_preview.rb
317
+ - spec/components/previews/better_ui/pagination/pagination_component_preview/all_sizes.html.erb
318
+ - spec/components/previews/better_ui/pagination/pagination_component_preview/all_styles.html.erb
319
+ - spec/components/previews/better_ui/pagination/pagination_component_preview/all_variants.html.erb
320
+ - spec/components/previews/better_ui/pagination/pagination_component_preview/default.html.erb
321
+ - spec/components/previews/better_ui/pagination/pagination_component_preview/edge_cases.html.erb
322
+ - spec/components/previews/better_ui/pagination/pagination_component_preview/with_info.html.erb
314
323
  - spec/components/previews/better_ui/progress_component_preview.rb
315
324
  - spec/components/previews/better_ui/progress_component_preview/all_sizes.html.erb
316
325
  - spec/components/previews/better_ui/progress_component_preview/all_variants.html.erb
@@ -327,6 +336,7 @@ files:
327
336
  - spec/components/previews/better_ui/table/table_component_preview/highlighted.html.erb
328
337
  - spec/components/previews/better_ui/table/table_component_preview/hoverable.html.erb
329
338
  - spec/components/previews/better_ui/table/table_component_preview/inside_card.html.erb
339
+ - spec/components/previews/better_ui/table/table_component_preview/row_html.html.erb
330
340
  - spec/components/previews/better_ui/table/table_component_preview/sortable.html.erb
331
341
  - spec/components/previews/better_ui/table/table_component_preview/striped.html.erb
332
342
  - spec/components/previews/better_ui/table/table_component_preview/with_footer.html.erb