Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
5f94eab
docs(spec): DataTable Hotwire-first design
djalmaaraujo Apr 24, 2026
a33cc9d
docs(plan): DataTable implementation plan (28 tasks)
djalmaaraujo Apr 24, 2026
5fc6f3f
chore(gitignore): exclude vendor/bundle; plan fixes
djalmaaraujo Apr 24, 2026
70bc609
feat(routes): data_table docs and demo endpoints
djalmaaraujo Apr 24, 2026
6f0b1ca
feat(docs): add data_table action to DocsController
djalmaaraujo Apr 24, 2026
f1c204c
feat(docs-nav): add DataTable sidebar entry
djalmaaraujo Apr 24, 2026
469d4d9
feat(docs): add DataTableDemoData module (100 employees)
djalmaaraujo Apr 24, 2026
ffc09bf
feat(data_table): add Manual pagination adapter
djalmaaraujo Apr 24, 2026
f30c091
docs(plan): fix adapter paths to match Zeitwerk layout
djalmaaraujo Apr 24, 2026
14585df
feat(data_table): add Pagy pagination adapter
djalmaaraujo Apr 24, 2026
27fb01c
feat(data_table): add Kaminari pagination adapter
djalmaaraujo Apr 24, 2026
1daa647
feat(data_table): add DataTable root component
djalmaaraujo Apr 24, 2026
dd51bfa
fix(data_table): narrow CSRF rescue + assert token in test
djalmaaraujo Apr 24, 2026
bc48574
feat(data_table): add DataTableToolbar layout slot
djalmaaraujo Apr 24, 2026
1e7597d
feat(data_table): add DataTableSearch (Turbo-Frame GET form)
djalmaaraujo Apr 24, 2026
dd3bad7
fix(data_table): use RubyUI::Input in Search (spec-consistent)
djalmaaraujo Apr 24, 2026
2fa86ea
feat(data_table): add DataTablePerPageSelect (auto-submitting select)
djalmaaraujo Apr 24, 2026
ede6b9b
feat(data_table): add DataTableSortHead with asc/desc/none cycle
djalmaaraujo Apr 24, 2026
9b08a04
feat(data_table): add DataTableRowCheckbox (form-first selection)
djalmaaraujo Apr 24, 2026
41af7be
feat(data_table): add DataTableSelectAllCheckbox
djalmaaraujo Apr 24, 2026
e8d7094
feat(data_table): add DataTableSelectionSummary
djalmaaraujo Apr 24, 2026
adb5ab3
feat(data_table): add DataTableBulkActions (hidden until selection>0)
djalmaaraujo Apr 24, 2026
04970f1
feat(data_table): add DataTableSelectionBar
djalmaaraujo Apr 24, 2026
5101de6
feat(data_table): add DataTableColumnToggle
djalmaaraujo Apr 24, 2026
975dd81
refactor(data_table): move adapters to DataTablePaginationAdapters
djalmaaraujo Apr 24, 2026
8da42a3
feat(data_table): add DataTablePagination with adapter support
djalmaaraujo Apr 24, 2026
6cbc896
feat(data_table): add data-table Stimulus controller
djalmaaraujo Apr 24, 2026
9d06753
feat(data_table): add data-table-column-visibility Stimulus controller
djalmaaraujo Apr 24, 2026
93d823d
feat(data_table): register data-table Stimulus controllers
djalmaaraujo Apr 24, 2026
9a3ed9d
feat(data_table): add demo controller with search/sort/paginate + bul…
djalmaaraujo Apr 24, 2026
a52dd29
feat(data_table): wire complete demo view
djalmaaraujo Apr 24, 2026
64e91f5
feat(docs): add DataTable docs page with 6 examples
djalmaaraujo Apr 24, 2026
555e177
test(components): only GET docs routes in generated tests
djalmaaraujo Apr 24, 2026
2fafab6
style(data_table): standardrb --fix
djalmaaraujo Apr 24, 2026
e8d9886
refactor(data_table): remove form wrapper from root, add DataTableForm
djalmaaraujo Apr 24, 2026
e4c9733
feat(data_table): wrap table + selection bar in DataTableForm in demo
djalmaaraujo Apr 24, 2026
395559f
fix(docs): make VisualCodeExample previews fill container width
djalmaaraujo Apr 24, 2026
499c1d6
docs(data_table): update example heredocs to show DataTableForm pattern
djalmaaraujo Apr 24, 2026
4d9d514
feat(data_table): add Stimulus debounce controller for search
djalmaaraujo Apr 24, 2026
72d56f7
feat(data_table): wire debounce prop on DataTableSearch
djalmaaraujo Apr 24, 2026
5762dda
fix(data_table): restore search focus + cursor after Turbo Frame swap
djalmaaraujo Apr 24, 2026
24eb85c
feat(data_table): DataTableForm id: prop + form= attribute support
djalmaaraujo Apr 24, 2026
6d02437
fix(data_table): keep selection summary always visible
djalmaaraujo Apr 24, 2026
3bccade
feat(data_table): move bulk actions into toolbar (demo layout)
djalmaaraujo Apr 24, 2026
efd890a
docs(data_table): update example heredocs for toolbar bulk actions
djalmaaraujo Apr 24, 2026
c4a5241
fix(data_table): per-page select chevron icon (avoid overlap)
djalmaaraujo Apr 24, 2026
fbf8a9c
fix(layout): widen docs main content
djalmaaraujo Apr 24, 2026
e9ed5b2
revert(data_table): per-page select uses native browser dropdown arrow
djalmaaraujo Apr 24, 2026
6b704fe
docs(data_table): document bulk action button attributes
djalmaaraujo Apr 24, 2026
a22acba
fix(data_table): restore NativeSelectIcon wrapper for per-page select
djalmaaraujo Apr 24, 2026
fe130d5
docs(data_table): menu + page title use 'Data Table' (two words)
djalmaaraujo Apr 24, 2026
bff7d81
style(table): TableHead uses text-foreground + whitespace-nowrap
djalmaaraujo Apr 24, 2026
90a3136
feat(data_table): add DataTablePaginationBar wrapper
djalmaaraujo Apr 24, 2026
ad375e7
feat(data_table): use DataTablePaginationBar in demo + docs
djalmaaraujo Apr 24, 2026
6cf1947
feat(data_table): add expandable row support (accessible)
djalmaaraujo Apr 24, 2026
fc7f3e3
fix(data_table): Pagination wrapper no longer forces center alignment
djalmaaraujo Apr 24, 2026
468dbd8
fix(lint): drop unused component var + prefer double quotes
djalmaaraujo Apr 24, 2026
65a4a21
revert(table): restore TableHead muted-foreground default
djalmaaraujo Apr 24, 2026
4632359
fix(data_table): SortHead honors configurable page_param when resetti…
djalmaaraujo Apr 24, 2026
10823c1
refactor(data_table): drop defensive view_context guards (fail loudly)
djalmaaraujo Apr 24, 2026
316545e
refactor(data_table): PerPageSelect uses RubyUI::NativeSelect
djalmaaraujo Apr 24, 2026
a73c6db
refactor(data_table): row + select-all checkboxes reuse RubyUI::Checkbox
djalmaaraujo Apr 24, 2026
b0b1b61
chore(data_table): remove unused DataTableSelectionBar
djalmaaraujo Apr 24, 2026
9b397d3
feat(data_table): Search preserves other query params via hidden inputs
djalmaaraujo Apr 24, 2026
a846235
refactor(data_table): delegate expand toggle to root controller
djalmaaraujo Apr 24, 2026
fa21462
refactor(data_table): search focus restore via turbo:before-frame-render
djalmaaraujo Apr 24, 2026
92d31c5
docs(data_table): note button_to alternative and column toggle reset
djalmaaraujo Apr 24, 2026
8df75ab
chore: remove docs/superpowers spec + plan artifacts
djalmaaraujo Apr 24, 2026
ca22440
chore: gitignore /docs/superpowers (local design artifacts)
djalmaaraujo Apr 24, 2026
3ecdd36
docs(data_table): remove basic static table example
djalmaaraujo Apr 24, 2026
aaf5cab
docs(data_table): explain pagination adapters (Manual/Pagy/Kaminari/c…
djalmaaraujo Apr 24, 2026
9e83f83
refactor(data_table): ivar @id instead of @frame_id
djalmaaraujo Apr 24, 2026
6fc14d4
refactor(data_table): configurable pagination window via kwarg
djalmaaraujo Apr 24, 2026
de85b6e
test(data_table): cover DataTableForm method: kwarg
djalmaaraujo Apr 24, 2026
2fa3dd3
refactor(data_table): use number_to_currency instead of regex reverse
djalmaaraujo Apr 24, 2026
4ff5510
test(data_table): assert row checkbox markup on index
djalmaaraujo Apr 24, 2026
8383fe2
refactor(data_table): drop redundant arbitrary selector in expand toggle
djalmaaraujo Apr 24, 2026
6bcf141
feat(data_table): row checkbox accepts label: kwarg for meaningful ar…
djalmaaraujo Apr 24, 2026
e7d8656
style: fix standardrb offenses (heredoc quotes, extra spacing, class …
djalmaaraujo Apr 24, 2026
112bd8e
fix(data_table): search focus survives Turbo Frame swap via module-le…
djalmaaraujo Apr 24, 2026
8abdf6f
fix(data_table): expand toggle arrow rotates via group-aria-expanded …
djalmaaraujo Apr 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

# Ignore bundler config.
/.bundle
/vendor/bundle

# Ignore all logfiles and tempfiles.
/log/*
Expand Down Expand Up @@ -46,3 +47,6 @@ yarn-error.log

# Pnpm
.pnpm-store

# Local design artifacts (not checked in)
/docs/superpowers/
6 changes: 3 additions & 3 deletions app/components/docs/visual_code_example.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,16 @@ def render_preview_tab(&block)
end

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

def raw_preview
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
div(class: "preview flex min-h-[350px] w-full justify-center p-10 items-center") do
div(class: "preview min-h-[350px] w-full p-6") do
decoded_code = CGI.unescapeHTML(@display_code)
@context.instance_eval(decoded_code)
end
Expand Down
29 changes: 29 additions & 0 deletions app/components/ruby_ui/data_table/data_table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

module RubyUI
class DataTable < Base
register_element :turbo_frame, tag: "turbo-frame"

def initialize(id:, **attrs)
@id = id
super(**attrs)
end

def view_template(&block)
turbo_frame(id: @id, target: "_top") do
div(**attrs) do
yield if block
end
end
end

private

def default_attrs
{
class: "w-full space-y-4",
data: {controller: "ruby-ui--data-table"}
}
end
end
end
18 changes: 18 additions & 0 deletions app/components/ruby_ui/data_table/data_table_bulk_actions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module RubyUI
class DataTableBulkActions < Base
def view_template(&)
div(**attrs, &)
end

private

def default_attrs
{
class: "hidden items-center gap-2",
data: {"ruby-ui--data-table-target": "bulkActions"}
}
end
end
end
48 changes: 48 additions & 0 deletions app/components/ruby_ui/data_table/data_table_column_toggle.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

module RubyUI
class DataTableColumnToggle < Base
def initialize(columns:, **attrs)
@columns = columns
super(**attrs)
end

def view_template
div(**attrs) do
render RubyUI::DropdownMenu.new do
render RubyUI::DropdownMenuTrigger.new do
render RubyUI::Button.new(variant: :outline, size: :sm) do
plain "Columns"
raw view_context.lucide_icon("chevron-down", class: "w-4 h-4 ml-1")
end
end
render RubyUI::DropdownMenuContent.new do
@columns.each do |col|
label(class: "flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm cursor-pointer hover:bg-accent") do
input(
type: "checkbox",
checked: true,
class: "h-4 w-4 rounded border border-input accent-primary cursor-pointer",
data: {
column_key: col[:key].to_s,
action: "change->ruby-ui--data-table-column-visibility#toggle"
}
)
span { plain col[:label] }
end
end
end
end
end
end

private

def default_attrs
{
class: "relative",
data: {controller: "ruby-ui--data-table-column-visibility"}
}
end
end
end
39 changes: 39 additions & 0 deletions app/components/ruby_ui/data_table/data_table_expand_toggle.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

module RubyUI
class DataTableExpandToggle < Base
def initialize(controls:, expanded: false, label: "Toggle row details", **attrs)
@controls = controls
@expanded = expanded
@label = label
super(**attrs)
end

def view_template
button(
type: "button",
aria_expanded: @expanded.to_s,
aria_controls: @controls,
aria_label: @label,
data: {
action: "click->ruby-ui--data-table#toggleRowDetail"
},
**attrs
) do
render_icon
end
end

private

def render_icon
raw view_context.lucide_icon("chevron-right", class: "h-4 w-4 transition-transform duration-150 group-aria-expanded:rotate-90")
end

def default_attrs
{
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"
}
end
end
end
31 changes: 31 additions & 0 deletions app/components/ruby_ui/data_table/data_table_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

module RubyUI
class DataTableForm < Base
def initialize(action: "", method: "post", id: nil, **attrs)
@action = action
@method = method
@id = id
super(**attrs)
end

def view_template(&block)
form_attrs = {action: @action, method: @method}
form_attrs[:id] = @id if @id
form(**form_attrs, **attrs) do
input(type: "hidden", name: "authenticity_token", value: csrf_token)
yield if block
end
end

private

def csrf_token
view_context.form_authenticity_token
end

def default_attrs
{}
end
end
end
84 changes: 84 additions & 0 deletions app/components/ruby_ui/data_table/data_table_pagination.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# frozen_string_literal: true

module RubyUI
class DataTablePagination < Base
def initialize(with: nil, pagy: nil, kaminari: nil, page: nil, per_page: nil, total_count: nil, page_param: "page", path: "", query: {}, window: 1, **attrs)
@adapter = resolve_adapter(with:, pagy:, kaminari:, page:, per_page:, total_count:)
@page_param = page_param
@path = path
@query = query.to_h.transform_keys(&:to_s)
@window = window
super(**attrs)
end

def view_template
render RubyUI::Pagination.new(class: "mx-0 w-auto justify-end", **attrs) do
render RubyUI::PaginationContent.new do
prev_item
number_items
next_item
end
end
end

private

def resolve_adapter(with:, pagy:, kaminari:, page:, per_page:, total_count:)
return with if with
return RubyUI::DataTablePaginationAdapters::Pagy.new(pagy) if pagy
return RubyUI::DataTablePaginationAdapters::Kaminari.new(kaminari) if kaminari
if page && per_page && total_count
return RubyUI::DataTablePaginationAdapters::Manual.new(page:, per_page:, total_count:)
end
raise ArgumentError, "DataTablePagination requires one of: with:, pagy:, kaminari:, or page:+per_page:+total_count:"
end

def current = @adapter.current_page
def total = @adapter.total_pages

def page_href(p)
qs = @query.merge(@page_param => p.to_s).to_query
qs.empty? ? @path : "#{@path}?#{qs}"
end

def prev_item
if current <= 1
li do
span(class: "opacity-50 pointer-events-none px-3 h-9 inline-flex items-center text-sm") { plain "Previous" }
end
else
render RubyUI::PaginationItem.new(href: page_href(current - 1)) { plain "Previous" }
end
end

def next_item
if current >= total
li do
span(class: "opacity-50 pointer-events-none px-3 h-9 inline-flex items-center text-sm") { plain "Next" }
end
else
render RubyUI::PaginationItem.new(href: page_href(current + 1)) { plain "Next" }
end
end

def number_items
windowed_pages.each do |p|
if p == :gap
render RubyUI::PaginationEllipsis.new
else
render RubyUI::PaginationItem.new(href: page_href(p), active: p == current) { plain p.to_s }
end
end
end

def windowed_pages
return (1..total).to_a if total <= 7
pages = [1]
pages << :gap if current - @window > 2
((current - @window)..(current + @window)).each { |p| pages << p if p > 1 && p < total }
pages << :gap if current + @window < total - 1
pages << total
pages
end
end
end
15 changes: 15 additions & 0 deletions app/components/ruby_ui/data_table/data_table_pagination_bar.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module RubyUI
class DataTablePaginationBar < Base
def view_template(&)
div(**attrs, &)
end

private

def default_attrs
{class: "flex items-center justify-between gap-4 py-2"}
end
end
end
35 changes: 35 additions & 0 deletions app/components/ruby_ui/data_table/data_table_per_page_select.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

module RubyUI
class DataTablePerPageSelect < Base
def initialize(path:, name: "per_page", value: nil, frame_id: nil, options: [5, 10, 25, 50], **attrs)
@path = path
@name = name
@value = value
@frame_id = frame_id
@options = options
super(**attrs)
end

def view_template
form_attrs = {action: @path, method: "get"}
form_attrs[:data] = {turbo_frame: @frame_id} if @frame_id

form(**attrs.merge(form_attrs)) do
render RubyUI::NativeSelect.new(name: @name, onchange: safe("this.form.requestSubmit()")) do
@options.each do |opt|
option_attrs = {value: opt.to_s}
option_attrs[:selected] = true if opt.to_s == @value.to_s
option(**option_attrs) { plain opt.to_s }
end
end
end
end

private

def default_attrs
{}
end
end
end
30 changes: 30 additions & 0 deletions app/components/ruby_ui/data_table/data_table_row_checkbox.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

module RubyUI
class DataTableRowCheckbox < Base
def initialize(value:, name: "ids[]", label: nil, **attrs)
@value = value
@name = name
@label = label
super(**attrs)
end

def view_template
render RubyUI::Checkbox.new(**attrs)
end

private

def default_attrs
{
name: @name,
value: @value,
aria_label: @label || "Select row #{@value}",
data: {
"ruby-ui--data-table-target": "rowCheckbox",
action: "change->ruby-ui--data-table#toggleRow"
}
}
end
end
end
Loading