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 +4 -4
- data/README.md +4 -0
- data/app/components/better_ui/pagination/pagination_component/pagination_component.html.erb +137 -0
- data/app/components/better_ui/pagination/pagination_component.rb +408 -0
- data/app/components/better_ui/table/table_component/table_component.html.erb +2 -2
- data/app/components/better_ui/table/table_component.rb +30 -0
- data/app/helpers/better_ui/application_helper.rb +74 -0
- data/lib/better_ui/version.rb +1 -1
- data/spec/components/previews/better_ui/pagination/pagination_component_preview/all_sizes.html.erb +13 -0
- data/spec/components/previews/better_ui/pagination/pagination_component_preview/all_styles.html.erb +13 -0
- data/spec/components/previews/better_ui/pagination/pagination_component_preview/all_variants.html.erb +13 -0
- data/spec/components/previews/better_ui/pagination/pagination_component_preview/default.html.erb +28 -0
- data/spec/components/previews/better_ui/pagination/pagination_component_preview/edge_cases.html.erb +82 -0
- data/spec/components/previews/better_ui/pagination/pagination_component_preview/with_info.html.erb +38 -0
- data/spec/components/previews/better_ui/pagination/pagination_component_preview.rb +88 -0
- data/spec/components/previews/better_ui/table/table_component_preview/row_html.html.erb +64 -0
- data/spec/components/previews/better_ui/table/table_component_preview.rb +6 -0
- metadata +11 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f946c8d53c3641506cd438e13df28f7cc23b3b341294ee790b8fb8ce6a3b5438
|
|
4
|
+
data.tar.gz: 0fa3b052a2588162f89d752460be51e318ed971152e6a903f9cfb76bbd2ca336
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/better_ui/version.rb
CHANGED
data/spec/components/previews/better_ui/pagination/pagination_component_preview/all_sizes.html.erb
ADDED
|
@@ -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>
|
data/spec/components/previews/better_ui/pagination/pagination_component_preview/all_styles.html.erb
ADDED
|
@@ -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>
|
data/spec/components/previews/better_ui/pagination/pagination_component_preview/default.html.erb
ADDED
|
@@ -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>
|
data/spec/components/previews/better_ui/pagination/pagination_component_preview/edge_cases.html.erb
ADDED
|
@@ -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>
|
data/spec/components/previews/better_ui/pagination/pagination_component_preview/with_info.html.erb
ADDED
|
@@ -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 — Displaying items 41–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>
|
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.
|
|
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
|