Skip to content
This repository was archived by the owner on May 11, 2026. It is now read-only.

Commit 0100625

Browse files
committed
[Feature] Add DataTable and NativeSelect component docs
Add documentation pages for the new DataTable component family (v1.2.0) with interactive demo featuring search, sort, pagination, column visibility, row selection, and bulk actions. Also copies NativeSelect component (DataTable dependency) and updates layout/preview for better table rendering.
1 parent 24d87d5 commit 0100625

32 files changed

Lines changed: 1609 additions & 132 deletions

Gemfile.lock

Lines changed: 121 additions & 127 deletions
Large diffs are not rendered by default.

app/components/docs/visual_code_example.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,16 +78,16 @@ def render_preview_tab(&block)
7878
end
7979

8080
def iframe_preview
81-
div(class: "relative aspect-[4/2.5] w-full overflow-hidden rounded-md border", data: {controller: "iframe-theme"}) do
82-
div(class: "absolute inset-0 hidden w-[1600px] bg-background md:block") do
81+
div(class: "relative min-h-[500px] w-full overflow-hidden rounded-md border", data: {controller: "iframe-theme"}) do
82+
div(class: "absolute inset-0 hidden w-full bg-background md:block") do
8383
iframe(src: @src, class: "size-full", data: {iframe_theme_target: "iframe"})
8484
end
8585
end
8686
end
8787

8888
def raw_preview
8989
div(class: "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 relative rounded-md border") do
90-
div(class: "preview flex min-h-[350px] w-full justify-center p-10 items-center") do
90+
div(class: "preview min-h-[350px] w-full p-6") do
9191
decoded_code = CGI.unescapeHTML(@display_code)
9292
@context.instance_eval(decoded_code)
9393
end
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# frozen_string_literal: true
2+
3+
module RubyUI
4+
class DataTable < Base
5+
register_element :turbo_frame, tag: "turbo-frame"
6+
7+
def initialize(id:, **attrs)
8+
@id = id
9+
super(**attrs)
10+
end
11+
12+
def view_template(&block)
13+
turbo_frame(id: @id, target: "_top") do
14+
div(**attrs) do
15+
yield if block
16+
end
17+
end
18+
end
19+
20+
private
21+
22+
def default_attrs
23+
{
24+
class: "w-full space-y-4",
25+
data: {controller: "ruby-ui--data-table"}
26+
}
27+
end
28+
end
29+
end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
3+
module RubyUI
4+
class DataTableBulkActions < Base
5+
def view_template(&)
6+
div(**attrs, &)
7+
end
8+
9+
private
10+
11+
def default_attrs
12+
{
13+
class: "hidden items-center gap-2",
14+
data: {"ruby-ui--data-table-target": "bulkActions"}
15+
}
16+
end
17+
end
18+
end
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# frozen_string_literal: true
2+
3+
module RubyUI
4+
class DataTableColumnToggle < Base
5+
def initialize(columns:, **attrs)
6+
@columns = columns
7+
super(**attrs)
8+
end
9+
10+
def view_template
11+
div(**attrs) do
12+
render RubyUI::DropdownMenu.new do
13+
render RubyUI::DropdownMenuTrigger.new do
14+
render RubyUI::Button.new(variant: :outline, size: :sm) do
15+
plain "Columns"
16+
# inline chevron-down SVG (lucide 24px, 1px stroke)
17+
svg(
18+
xmlns: "http://www.w3.org/2000/svg",
19+
width: "16",
20+
height: "16",
21+
viewBox: "0 0 24 24",
22+
fill: "none",
23+
stroke: "currentColor",
24+
stroke_width: "2",
25+
stroke_linecap: "round",
26+
stroke_linejoin: "round",
27+
class: "w-4 h-4 ml-1"
28+
) do |s|
29+
s.polyline(points: "6 9 12 15 18 9")
30+
end
31+
end
32+
end
33+
render RubyUI::DropdownMenuContent.new do
34+
@columns.each do |col|
35+
label(class: "flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm cursor-pointer hover:bg-accent") do
36+
input(
37+
type: "checkbox",
38+
checked: true,
39+
class: "h-4 w-4 rounded border border-input accent-primary cursor-pointer",
40+
data: {
41+
column_key: col[:key].to_s,
42+
action: "change->ruby-ui--data-table-column-visibility#toggle"
43+
}
44+
)
45+
span { plain col[:label] }
46+
end
47+
end
48+
end
49+
end
50+
end
51+
end
52+
53+
private
54+
55+
def default_attrs
56+
{
57+
class: "relative",
58+
data: {controller: "ruby-ui--data-table-column-visibility"}
59+
}
60+
end
61+
end
62+
end
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# frozen_string_literal: true
2+
3+
module RubyUI
4+
class DataTableExpandToggle < Base
5+
def initialize(controls:, expanded: false, label: "Toggle row details", **attrs)
6+
@controls = controls
7+
@expanded = expanded
8+
@label = label
9+
super(**attrs)
10+
end
11+
12+
def view_template
13+
button(
14+
type: "button",
15+
aria_expanded: @expanded.to_s,
16+
aria_controls: @controls,
17+
aria_label: @label,
18+
data: {
19+
action: "click->ruby-ui--data-table#toggleRowDetail"
20+
},
21+
**attrs
22+
) do
23+
render_icon
24+
end
25+
end
26+
27+
private
28+
29+
def render_icon
30+
# inline chevron-right SVG (lucide)
31+
svg(
32+
xmlns: "http://www.w3.org/2000/svg",
33+
width: "16",
34+
height: "16",
35+
viewBox: "0 0 24 24",
36+
fill: "none",
37+
stroke: "currentColor",
38+
stroke_width: "2",
39+
stroke_linecap: "round",
40+
stroke_linejoin: "round",
41+
class: "h-4 w-4 transition-transform duration-150 group-aria-expanded:rotate-90"
42+
) do |s|
43+
s.polyline(points: "9 18 15 12 9 6")
44+
end
45+
end
46+
47+
def default_attrs
48+
{
49+
class: "group inline-flex items-center justify-center h-8 w-8 rounded-md hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
50+
}
51+
end
52+
end
53+
end
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# frozen_string_literal: true
2+
3+
module RubyUI
4+
class DataTableForm < Base
5+
def initialize(action: "", method: "post", id: nil, **attrs)
6+
@action = action
7+
@method = method
8+
@id = id
9+
super(**attrs)
10+
end
11+
12+
def view_template(&block)
13+
form_attrs = {action: @action, method: @method}
14+
form_attrs[:id] = @id if @id
15+
form(**form_attrs, **attrs) do
16+
input(type: "hidden", name: "authenticity_token", value: csrf_token)
17+
yield if block
18+
end
19+
end
20+
21+
private
22+
23+
def csrf_token
24+
# In a Rails app, view_context provides a real CSRF token.
25+
# Outside Rails (gem tests), fall back to a placeholder.
26+
if respond_to?(:helpers, true) && helpers.respond_to?(:form_authenticity_token)
27+
helpers.form_authenticity_token
28+
elsif respond_to?(:view_context, true) && view_context.respond_to?(:form_authenticity_token)
29+
view_context.form_authenticity_token
30+
else
31+
"csrf-token-placeholder"
32+
end
33+
end
34+
35+
def default_attrs
36+
{}
37+
end
38+
end
39+
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# frozen_string_literal: true
2+
3+
module RubyUI
4+
class DataTableKaminariAdapter
5+
def initialize(collection)
6+
@collection = collection
7+
end
8+
9+
def current_page = @collection.current_page
10+
11+
def total_pages = @collection.total_pages
12+
13+
def total_count = @collection.total_count
14+
15+
def per_page = @collection.limit_value
16+
end
17+
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# frozen_string_literal: true
2+
3+
module RubyUI
4+
class DataTableManualAdapter
5+
attr_reader :current_page, :per_page, :total_count
6+
7+
def initialize(page:, per_page:, total_count:)
8+
@current_page = page.to_i
9+
@per_page = [per_page.to_i, 1].max
10+
@total_count = total_count.to_i
11+
end
12+
13+
def total_pages
14+
[(@total_count.to_f / @per_page).ceil, 1].max
15+
end
16+
end
17+
end
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# frozen_string_literal: true
2+
3+
require "cgi"
4+
require_relative "data_table_manual_adapter"
5+
require_relative "data_table_pagy_adapter"
6+
require_relative "data_table_kaminari_adapter"
7+
8+
module RubyUI
9+
class DataTablePagination < Base
10+
def initialize(with: nil, pagy: nil, kaminari: nil, page: nil, per_page: nil, total_count: nil, page_param: "page", path: "", query: {}, window: 1, prev_label: "<", next_label: ">", **attrs)
11+
@adapter = resolve_adapter(with:, pagy:, kaminari:, page:, per_page:, total_count:)
12+
@page_param = page_param
13+
@path = path
14+
@query = query.to_h.transform_keys(&:to_s)
15+
@window = window
16+
@prev_label = prev_label
17+
@next_label = next_label
18+
super(**attrs)
19+
end
20+
21+
def view_template
22+
return if total <= 1
23+
24+
render RubyUI::Pagination.new(class: "mx-0 w-auto justify-end", **attrs) do
25+
render RubyUI::PaginationContent.new do
26+
prev_item
27+
number_items
28+
next_item
29+
end
30+
end
31+
end
32+
33+
private
34+
35+
def resolve_adapter(with:, pagy:, kaminari:, page:, per_page:, total_count:)
36+
return with if with
37+
return RubyUI::DataTablePagyAdapter.new(pagy) if pagy
38+
return RubyUI::DataTableKaminariAdapter.new(kaminari) if kaminari
39+
if page && per_page && total_count
40+
return RubyUI::DataTableManualAdapter.new(page:, per_page:, total_count:)
41+
end
42+
raise ArgumentError, "DataTablePagination requires one of: with:, pagy:, kaminari:, or page:+per_page:+total_count:"
43+
end
44+
45+
def current = @adapter.current_page
46+
47+
def total = @adapter.total_pages
48+
49+
def page_href(p)
50+
qs = build_query(@query.merge(@page_param => p.to_s))
51+
qs.empty? ? @path : "#{@path}?#{qs}"
52+
end
53+
54+
def build_query(hash)
55+
hash.flat_map { |k, v|
56+
Array(v).map { |val| "#{CGI.escape(k.to_s)}=#{CGI.escape(val.to_s)}" }
57+
}.join("&")
58+
end
59+
60+
def prev_item
61+
if current <= 1
62+
li do
63+
span(class: "opacity-50 pointer-events-none px-3 h-9 inline-flex items-center text-sm") { @prev_label }
64+
end
65+
else
66+
render RubyUI::PaginationItem.new(href: page_href(current - 1)) { @prev_label }
67+
end
68+
end
69+
70+
def next_item
71+
if current >= total
72+
li do
73+
span(class: "opacity-50 pointer-events-none px-3 h-9 inline-flex items-center text-sm") { @next_label }
74+
end
75+
else
76+
render RubyUI::PaginationItem.new(href: page_href(current + 1)) { @next_label }
77+
end
78+
end
79+
80+
def number_items
81+
windowed_pages.each do |p|
82+
if p == :gap
83+
render RubyUI::PaginationEllipsis.new
84+
else
85+
render RubyUI::PaginationItem.new(href: page_href(p), active: p == current) { plain p.to_s }
86+
end
87+
end
88+
end
89+
90+
def windowed_pages
91+
return (1..total).to_a if total <= 7
92+
pages = [1]
93+
pages << :gap if current - @window > 2
94+
((current - @window)..(current + @window)).each { |p| pages << p if p > 1 && p < total }
95+
pages << :gap if current + @window < total - 1
96+
pages << total
97+
pages
98+
end
99+
end
100+
end

0 commit comments

Comments
 (0)